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