cuelang.org/go@v0.13.0/cue/errors/errors.go (about) 1 // Copyright 2018 The CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package errors defines shared types for handling CUE errors. 16 // 17 // The pivotal error type in CUE packages is the interface type Error. 18 // The information available in such errors can be most easily retrieved using 19 // the Path, Positions, and Print functions. 20 package errors 21 22 import ( 23 "cmp" 24 "errors" 25 "fmt" 26 "io" 27 "path/filepath" 28 "slices" 29 "strings" 30 31 "cuelang.org/go/cue/token" 32 ) 33 34 // New is a convenience wrapper for [errors.New] in the core library. 35 // It does not return a CUE error. 36 func New(msg string) error { 37 return errors.New(msg) 38 } 39 40 // Unwrap returns the result of calling the Unwrap method on err, if err 41 // implements Unwrap. Otherwise, Unwrap returns nil. 42 func Unwrap(err error) error { 43 return errors.Unwrap(err) 44 } 45 46 // Is reports whether any error in err's chain matches target. 47 // 48 // An error is considered to match a target if it is equal to that target or if 49 // it implements a method Is(error) bool such that Is(target) returns true. 50 func Is(err, target error) bool { 51 return errors.Is(err, target) 52 } 53 54 // As finds the first error in err's chain that matches the type to which target 55 // points, and if so, sets the target to its value and returns true. An error 56 // matches a type if it is assignable to the target type, or if it has a method 57 // As(interface{}) bool such that As(target) returns true. As will panic if 58 // target is not a non-nil pointer to a type which implements error or is of 59 // interface type. 60 // 61 // The As method should set the target to its value and return true if err 62 // matches the type to which target points. 63 func As(err error, target interface{}) bool { 64 return errors.As(err, target) 65 } 66 67 // A Message implements the error interface as well as Message to allow 68 // internationalized messages. A Message is typically used as an embedding 69 // in a CUE message. 70 type Message struct { 71 format string 72 args []interface{} 73 } 74 75 // NewMessagef creates an error message for human consumption. The arguments 76 // are for later consumption, allowing the message to be localized at a later 77 // time. The passed argument list should not be modified. 78 func NewMessagef(format string, args ...interface{}) Message { 79 if false { 80 // Let go vet know that we're expecting printf-like arguments. 81 _ = fmt.Sprintf(format, args...) 82 } 83 return Message{format: format, args: args} 84 } 85 86 // NewMessage creates an error message for human consumption. 87 // 88 // Deprecated: Use [NewMessagef] instead. 89 func NewMessage(format string, args []interface{}) Message { 90 return NewMessagef(format, args...) 91 } 92 93 // Msg returns a printf-style format string and its arguments for human 94 // consumption. 95 func (m *Message) Msg() (format string, args []interface{}) { 96 return m.format, m.args 97 } 98 99 func (m *Message) Error() string { 100 return fmt.Sprintf(m.format, m.args...) 101 } 102 103 // Error is the common error message. 104 type Error interface { 105 // Position returns the primary position of an error. If multiple positions 106 // contribute equally, this reflects one of them. 107 Position() token.Pos 108 109 // InputPositions reports positions that contributed to an error, including 110 // the expressions resulting in the conflict, as well as values that were 111 // the input to this expression. 112 InputPositions() []token.Pos 113 114 // Error reports the error message without position information. 115 Error() string 116 117 // Path returns the path into the data tree where the error occurred. 118 // This path may be nil if the error is not associated with such a location. 119 Path() []string 120 121 // Msg returns the unformatted error message and its arguments for human 122 // consumption. 123 Msg() (format string, args []interface{}) 124 } 125 126 // Positions returns all positions returned by an error, sorted 127 // by relevance when possible and with duplicates removed. 128 func Positions(err error) []token.Pos { 129 e := Error(nil) 130 if !errors.As(err, &e) { 131 return nil 132 } 133 134 a := make([]token.Pos, 0, 3) 135 136 pos := e.Position() 137 if pos.IsValid() { 138 a = append(a, pos) 139 } 140 sortOffset := len(a) 141 142 for _, p := range e.InputPositions() { 143 if p.IsValid() && p != pos { 144 a = append(a, p) 145 } 146 } 147 148 slices.SortFunc(a[sortOffset:], comparePosWithNoPosFirst) 149 return slices.Compact(a) 150 } 151 152 // comparePosWithNoPosFirst wraps [token.Pos.Compare] to place [token.NoPos] first, 153 // which is currently required for errors to be sorted correctly. 154 // TODO: give all errors valid positions so that we can use the standard sorting directly. 155 func comparePosWithNoPosFirst(a, b token.Pos) int { 156 if a == b { 157 return 0 158 } else if a == token.NoPos { 159 return -1 160 } else if b == token.NoPos { 161 return +1 162 } 163 return token.Pos.Compare(a, b) 164 } 165 166 // Path returns the path of an Error if err is of that type. 167 func Path(err error) []string { 168 if e := Error(nil); errors.As(err, &e) { 169 return e.Path() 170 } 171 return nil 172 } 173 174 // Newf creates an Error with the associated position and message. 175 func Newf(p token.Pos, format string, args ...interface{}) Error { 176 return &posError{ 177 pos: p, 178 Message: NewMessagef(format, args...), 179 } 180 } 181 182 // Wrapf creates an Error with the associated position and message. The provided 183 // error is added for inspection context. 184 func Wrapf(err error, p token.Pos, format string, args ...interface{}) Error { 185 pErr := &posError{ 186 pos: p, 187 Message: NewMessagef(format, args...), 188 } 189 return Wrap(pErr, err) 190 } 191 192 // Wrap creates a new error where child is a subordinate error of parent. 193 // If child is list of Errors, the result will itself be a list of errors 194 // where child is a subordinate error of each parent. 195 func Wrap(parent Error, child error) Error { 196 if child == nil { 197 return parent 198 } 199 a, ok := child.(list) 200 if !ok { 201 return &wrapped{parent, child} 202 } 203 b := make(list, len(a)) 204 for i, err := range a { 205 b[i] = &wrapped{parent, err} 206 } 207 return b 208 } 209 210 type wrapped struct { 211 main Error 212 wrap error 213 } 214 215 // Error implements the error interface. 216 func (e *wrapped) Error() string { 217 switch msg := e.main.Error(); { 218 case e.wrap == nil: 219 return msg 220 case msg == "": 221 return e.wrap.Error() 222 default: 223 return fmt.Sprintf("%s: %s", msg, e.wrap) 224 } 225 } 226 227 func (e *wrapped) Is(target error) bool { 228 return Is(e.main, target) 229 } 230 231 func (e *wrapped) As(target interface{}) bool { 232 return As(e.main, target) 233 } 234 235 func (e *wrapped) Msg() (format string, args []interface{}) { 236 return e.main.Msg() 237 } 238 239 func (e *wrapped) Path() []string { 240 if p := e.main.Path(); p != nil { 241 return p 242 } 243 return Path(e.wrap) 244 } 245 246 func (e *wrapped) InputPositions() []token.Pos { 247 return append(e.main.InputPositions(), Positions(e.wrap)...) 248 } 249 250 func (e *wrapped) Position() token.Pos { 251 if p := e.main.Position(); p != token.NoPos { 252 return p 253 } 254 if wrap, ok := e.wrap.(Error); ok { 255 return wrap.Position() 256 } 257 return token.NoPos 258 } 259 260 func (e *wrapped) Unwrap() error { return e.wrap } 261 262 func (e *wrapped) Cause() error { return e.wrap } 263 264 // Promote converts a regular Go error to an Error if it isn't already one. 265 func Promote(err error, msg string) Error { 266 switch x := err.(type) { 267 case Error: 268 return x 269 default: 270 return Wrapf(err, token.NoPos, "%s", msg) 271 } 272 } 273 274 var _ Error = &posError{} 275 276 // In an List, an error is represented by an *posError. 277 // The position Pos, if valid, points to the beginning of 278 // the offending token, and the error condition is described 279 // by Msg. 280 type posError struct { 281 pos token.Pos 282 Message 283 } 284 285 func (e *posError) Path() []string { return nil } 286 func (e *posError) InputPositions() []token.Pos { return nil } 287 func (e *posError) Position() token.Pos { return e.pos } 288 289 // Append combines two errors, flattening Lists as necessary. 290 func Append(a, b Error) Error { 291 switch x := a.(type) { 292 case nil: 293 return b 294 case list: 295 return appendToList(x, b) 296 } 297 // Preserve order of errors. 298 return appendToList(list{a}, b) 299 } 300 301 // Errors reports the individual errors associated with an error, which is 302 // the error itself if there is only one or, if the underlying type is List, 303 // its individual elements. If the given error is not an Error, it will be 304 // promoted to one. 305 func Errors(err error) []Error { 306 if err == nil { 307 return nil 308 } 309 var listErr list 310 var errorErr Error 311 switch { 312 case As(err, &listErr): 313 return listErr 314 case As(err, &errorErr): 315 return []Error{errorErr} 316 default: 317 return []Error{Promote(err, "")} 318 } 319 } 320 321 func appendToList(a list, err Error) list { 322 switch x := err.(type) { 323 case nil: 324 return a 325 case list: 326 if len(a) == 0 { 327 return x 328 } 329 for _, e := range x { 330 a = appendToList(a, e) 331 } 332 return a 333 default: 334 for _, e := range a { 335 if e == err { 336 return a 337 } 338 } 339 return append(a, err) 340 } 341 } 342 343 // list is a list of Errors. 344 // The zero value for an list is an empty list ready to use. 345 type list []Error 346 347 func (p list) Is(target error) bool { 348 for _, e := range p { 349 if errors.Is(e, target) { 350 return true 351 } 352 } 353 return false 354 } 355 356 func (p list) As(target interface{}) bool { 357 for _, e := range p { 358 if errors.As(e, target) { 359 return true 360 } 361 } 362 return false 363 } 364 365 // AddNewf adds an Error with given position and error message to an List. 366 func (p *list) AddNewf(pos token.Pos, msg string, args ...interface{}) { 367 err := &posError{pos: pos, Message: Message{format: msg, args: args}} 368 *p = append(*p, err) 369 } 370 371 // Add adds an Error with given position and error message to an List. 372 func (p *list) Add(err Error) { 373 *p = appendToList(*p, err) 374 } 375 376 // Reset resets an List to no errors. 377 func (p *list) Reset() { *p = (*p)[:0] } 378 379 // Sanitize sorts multiple errors and removes duplicates on a best effort basis. 380 // If err represents a single or no error, it returns the error as is. 381 func Sanitize(err Error) Error { 382 if err == nil { 383 return nil 384 } 385 if l, ok := err.(list); ok { 386 a := l.sanitize() 387 if len(a) == 1 { 388 return a[0] 389 } 390 return a 391 } 392 return err 393 } 394 395 func (p list) sanitize() list { 396 if p == nil { 397 return p 398 } 399 a := slices.Clone(p) 400 a.RemoveMultiples() 401 return a 402 } 403 404 // Sort sorts an List. *posError entries are sorted by position, 405 // other errors are sorted by error message, and before any *posError 406 // entry. 407 func (p list) Sort() { 408 slices.SortFunc(p, func(a, b Error) int { 409 if c := comparePosWithNoPosFirst(a.Position(), b.Position()); c != 0 { 410 return c 411 } 412 if c := slices.Compare(a.Path(), b.Path()); c != 0 { 413 return c 414 } 415 return cmp.Compare(a.Error(), b.Error()) 416 417 }) 418 } 419 420 // RemoveMultiples sorts an List and removes all but the first error per line. 421 func (p *list) RemoveMultiples() { 422 p.Sort() 423 *p = slices.CompactFunc(*p, approximateEqual) 424 } 425 426 func approximateEqual(a, b Error) bool { 427 aPos := a.Position() 428 bPos := b.Position() 429 if aPos == token.NoPos || bPos == token.NoPos { 430 return a.Error() == b.Error() 431 } 432 return comparePosWithNoPosFirst(aPos, bPos) == 0 && slices.Compare(a.Path(), b.Path()) == 0 433 } 434 435 // An List implements the error interface. 436 func (p list) Error() string { 437 format, args := p.Msg() 438 return fmt.Sprintf(format, args...) 439 } 440 441 // Msg reports the unformatted error message for the first error, if any. 442 func (p list) Msg() (format string, args []interface{}) { 443 switch len(p) { 444 case 0: 445 return "no errors", nil 446 case 1: 447 return p[0].Msg() 448 } 449 return "%s (and %d more errors)", []interface{}{p[0], len(p) - 1} 450 } 451 452 // Position reports the primary position for the first error, if any. 453 func (p list) Position() token.Pos { 454 if len(p) == 0 { 455 return token.NoPos 456 } 457 return p[0].Position() 458 } 459 460 // InputPositions reports the input positions for the first error, if any. 461 func (p list) InputPositions() []token.Pos { 462 if len(p) == 0 { 463 return nil 464 } 465 return p[0].InputPositions() 466 } 467 468 // Path reports the path location of the first error, if any. 469 func (p list) Path() []string { 470 if len(p) == 0 { 471 return nil 472 } 473 return p[0].Path() 474 } 475 476 // Err returns an error equivalent to this error list. 477 // If the list is empty, Err returns nil. 478 func (p list) Err() error { 479 if len(p) == 0 { 480 return nil 481 } 482 return p 483 } 484 485 // A Config defines parameters for printing. 486 type Config struct { 487 // Format formats the given string and arguments and writes it to w. 488 // It is used for all printing. 489 Format func(w io.Writer, format string, args ...interface{}) 490 491 // Cwd is the current working directory. Filename positions are taken 492 // relative to this path. 493 Cwd string 494 495 // ToSlash sets whether to use Unix paths. Mostly used for testing. 496 ToSlash bool 497 } 498 499 var zeroConfig = &Config{} 500 501 // Print is a utility function that prints a list of errors to w, 502 // one error per line, if the err parameter is an List. Otherwise 503 // it prints the err string. 504 func Print(w io.Writer, err error, cfg *Config) { 505 if cfg == nil { 506 cfg = zeroConfig 507 } 508 for _, e := range list(Errors(err)).sanitize() { 509 printError(w, e, cfg) 510 } 511 } 512 513 // Details is a convenience wrapper for Print to return the error text as a 514 // string. 515 func Details(err error, cfg *Config) string { 516 var b strings.Builder 517 Print(&b, err, cfg) 518 return b.String() 519 } 520 521 // String generates a short message from a given Error. 522 func String(err Error) string { 523 var b strings.Builder 524 writeErr(&b, err, zeroConfig) 525 return b.String() 526 } 527 528 func writeErr(w io.Writer, err Error, cfg *Config) { 529 if path := strings.Join(err.Path(), "."); path != "" { 530 _, _ = io.WriteString(w, path) 531 _, _ = io.WriteString(w, ": ") 532 } 533 534 for { 535 u := errors.Unwrap(err) 536 537 msg, args := err.Msg() 538 539 // Just like [printError] does when printing one position per line, 540 // make sure that any position formatting arguments print as relative paths. 541 // 542 // Note that [Error.Msg] isn't clear about whether we should treat args as read-only, 543 // so we make a copy if we need to replace any arguments. 544 didCopy := false 545 for i, arg := range args { 546 var pos token.Position 547 switch arg := arg.(type) { 548 case token.Pos: 549 pos = arg.Position() 550 case token.Position: 551 pos = arg 552 default: 553 continue 554 } 555 if !didCopy { 556 args = slices.Clone(args) 557 didCopy = true 558 } 559 pos.Filename = relPath(pos.Filename, cfg) 560 args[i] = pos 561 } 562 563 n, _ := fmt.Fprintf(w, msg, args...) 564 565 if u == nil { 566 break 567 } 568 569 if n > 0 { 570 _, _ = io.WriteString(w, ": ") 571 } 572 err, _ = u.(Error) 573 if err == nil { 574 fmt.Fprint(w, u) 575 break 576 } 577 } 578 } 579 580 func defaultFprintf(w io.Writer, format string, args ...interface{}) { 581 fmt.Fprintf(w, format, args...) 582 } 583 584 func printError(w io.Writer, err error, cfg *Config) { 585 if err == nil { 586 return 587 } 588 fprintf := cfg.Format 589 if fprintf == nil { 590 fprintf = defaultFprintf 591 } 592 593 if e, ok := err.(Error); ok { 594 writeErr(w, e, cfg) 595 } else { 596 fprintf(w, "%v", err) 597 } 598 599 positions := Positions(err) 600 if len(positions) == 0 { 601 fprintf(w, "\n") 602 return 603 } 604 fprintf(w, ":\n") 605 for _, p := range positions { 606 pos := p.Position() 607 path := relPath(pos.Filename, cfg) 608 fprintf(w, " %s", path) 609 if pos.IsValid() { 610 if path != "" { 611 fprintf(w, ":") 612 } 613 fprintf(w, "%d:%d", pos.Line, pos.Column) 614 } 615 fprintf(w, "\n") 616 } 617 } 618 619 func relPath(path string, cfg *Config) string { 620 if cfg.Cwd != "" { 621 if p, err := filepath.Rel(cfg.Cwd, path); err == nil { 622 path = p 623 // Some IDEs (e.g. VSCode) only recognize a path if it starts 624 // with a dot. This also helps to distinguish between local 625 // files and builtin packages. 626 if !strings.HasPrefix(path, ".") { 627 path = fmt.Sprintf(".%c%s", filepath.Separator, path) 628 } 629 } 630 } 631 if cfg.ToSlash { 632 path = filepath.ToSlash(path) 633 } 634 return path 635 }