cuelang.org/go@v0.10.1/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:], comparePos) 149 return slices.Compact(a) 150 } 151 152 // Path returns the path of an Error if err is of that type. 153 func Path(err error) []string { 154 if e := Error(nil); errors.As(err, &e) { 155 return e.Path() 156 } 157 return nil 158 } 159 160 // Newf creates an Error with the associated position and message. 161 func Newf(p token.Pos, format string, args ...interface{}) Error { 162 return &posError{ 163 pos: p, 164 Message: NewMessagef(format, args...), 165 } 166 } 167 168 // Wrapf creates an Error with the associated position and message. The provided 169 // error is added for inspection context. 170 func Wrapf(err error, p token.Pos, format string, args ...interface{}) Error { 171 pErr := &posError{ 172 pos: p, 173 Message: NewMessagef(format, args...), 174 } 175 return Wrap(pErr, err) 176 } 177 178 // Wrap creates a new error where child is a subordinate error of parent. 179 // If child is list of Errors, the result will itself be a list of errors 180 // where child is a subordinate error of each parent. 181 func Wrap(parent Error, child error) Error { 182 if child == nil { 183 return parent 184 } 185 a, ok := child.(list) 186 if !ok { 187 return &wrapped{parent, child} 188 } 189 b := make(list, len(a)) 190 for i, err := range a { 191 b[i] = &wrapped{parent, err} 192 } 193 return b 194 } 195 196 type wrapped struct { 197 main Error 198 wrap error 199 } 200 201 // Error implements the error interface. 202 func (e *wrapped) Error() string { 203 switch msg := e.main.Error(); { 204 case e.wrap == nil: 205 return msg 206 case msg == "": 207 return e.wrap.Error() 208 default: 209 return fmt.Sprintf("%s: %s", msg, e.wrap) 210 } 211 } 212 213 func (e *wrapped) Is(target error) bool { 214 return Is(e.main, target) 215 } 216 217 func (e *wrapped) As(target interface{}) bool { 218 return As(e.main, target) 219 } 220 221 func (e *wrapped) Msg() (format string, args []interface{}) { 222 return e.main.Msg() 223 } 224 225 func (e *wrapped) Path() []string { 226 if p := e.main.Path(); p != nil { 227 return p 228 } 229 return Path(e.wrap) 230 } 231 232 func (e *wrapped) InputPositions() []token.Pos { 233 return append(e.main.InputPositions(), Positions(e.wrap)...) 234 } 235 236 func (e *wrapped) Position() token.Pos { 237 if p := e.main.Position(); p != token.NoPos { 238 return p 239 } 240 if wrap, ok := e.wrap.(Error); ok { 241 return wrap.Position() 242 } 243 return token.NoPos 244 } 245 246 func (e *wrapped) Unwrap() error { return e.wrap } 247 248 func (e *wrapped) Cause() error { return e.wrap } 249 250 // Promote converts a regular Go error to an Error if it isn't already one. 251 func Promote(err error, msg string) Error { 252 switch x := err.(type) { 253 case Error: 254 return x 255 default: 256 return Wrapf(err, token.NoPos, "%s", msg) 257 } 258 } 259 260 var _ Error = &posError{} 261 262 // In an List, an error is represented by an *posError. 263 // The position Pos, if valid, points to the beginning of 264 // the offending token, and the error condition is described 265 // by Msg. 266 type posError struct { 267 pos token.Pos 268 Message 269 } 270 271 func (e *posError) Path() []string { return nil } 272 func (e *posError) InputPositions() []token.Pos { return nil } 273 func (e *posError) Position() token.Pos { return e.pos } 274 275 // Append combines two errors, flattening Lists as necessary. 276 func Append(a, b Error) Error { 277 switch a := a.(type) { 278 case nil: 279 return b 280 case list: 281 return appendToList(a, b) 282 } 283 switch b := b.(type) { 284 case nil: 285 return a 286 case list: 287 return appendToList(list{a}, b) 288 } 289 if a == b { 290 return a 291 } 292 return list{a, b} 293 } 294 295 // Errors reports the individual errors associated with an error, which is 296 // the error itself if there is only one or, if the underlying type is List, 297 // its individual elements. If the given error is not an Error, it will be 298 // promoted to one. 299 func Errors(err error) []Error { 300 if err == nil { 301 return nil 302 } 303 var listErr list 304 var errorErr Error 305 switch { 306 case As(err, &listErr): 307 return listErr 308 case As(err, &errorErr): 309 return []Error{errorErr} 310 default: 311 return []Error{Promote(err, "")} 312 } 313 } 314 315 func appendToList(a list, err Error) list { 316 switch x := err.(type) { 317 case nil: 318 return a 319 case list: 320 if a == nil { 321 return x 322 } 323 return append(a, x...) 324 default: 325 return append(a, err) 326 } 327 } 328 329 // list is a list of Errors. 330 // The zero value for an list is an empty list ready to use. 331 type list []Error 332 333 func (p list) Is(target error) bool { 334 for _, e := range p { 335 if errors.Is(e, target) { 336 return true 337 } 338 } 339 return false 340 } 341 342 func (p list) As(target interface{}) bool { 343 for _, e := range p { 344 if errors.As(e, target) { 345 return true 346 } 347 } 348 return false 349 } 350 351 // AddNewf adds an Error with given position and error message to an List. 352 func (p *list) AddNewf(pos token.Pos, msg string, args ...interface{}) { 353 err := &posError{pos: pos, Message: Message{format: msg, args: args}} 354 *p = append(*p, err) 355 } 356 357 // Add adds an Error with given position and error message to an List. 358 func (p *list) Add(err Error) { 359 *p = appendToList(*p, err) 360 } 361 362 // Reset resets an List to no errors. 363 func (p *list) Reset() { *p = (*p)[:0] } 364 365 func comparePos(a, b token.Pos) int { 366 if c := cmp.Compare(a.Filename(), b.Filename()); c != 0 { 367 return c 368 } 369 if c := cmp.Compare(a.Line(), b.Line()); c != 0 { 370 return c 371 } 372 return cmp.Compare(a.Column(), b.Column()) 373 } 374 375 func comparePath(a, b []string) int { 376 for i, x := range a { 377 if i >= len(b) { 378 break 379 } 380 if c := cmp.Compare(x, b[i]); c != 0 { 381 return c 382 } 383 } 384 return cmp.Compare(len(a), len(b)) 385 } 386 387 // Sanitize sorts multiple errors and removes duplicates on a best effort basis. 388 // If err represents a single or no error, it returns the error as is. 389 func Sanitize(err Error) Error { 390 if err == nil { 391 return nil 392 } 393 if l, ok := err.(list); ok { 394 a := l.sanitize() 395 if len(a) == 1 { 396 return a[0] 397 } 398 return a 399 } 400 return err 401 } 402 403 func (p list) sanitize() list { 404 if p == nil { 405 return p 406 } 407 a := slices.Clone(p) 408 a.RemoveMultiples() 409 return a 410 } 411 412 // Sort sorts an List. *posError entries are sorted by position, 413 // other errors are sorted by error message, and before any *posError 414 // entry. 415 func (p list) Sort() { 416 slices.SortFunc(p, func(a, b Error) int { 417 if c := comparePos(a.Position(), b.Position()); c != 0 { 418 return c 419 } 420 // Note that it is not sufficient to simply compare file offsets because 421 // the offsets do not reflect modified line information (through //line 422 // comments). 423 if c := comparePath(a.Path(), b.Path()); c != 0 { 424 return c 425 } 426 return cmp.Compare(a.Error(), b.Error()) 427 428 }) 429 } 430 431 // RemoveMultiples sorts an List and removes all but the first error per line. 432 func (p *list) RemoveMultiples() { 433 p.Sort() 434 var last Error 435 i := 0 436 for _, e := range *p { 437 if last == nil || !approximateEqual(last, e) { 438 last = e 439 (*p)[i] = e 440 i++ 441 } 442 } 443 (*p) = (*p)[0:i] 444 } 445 446 func approximateEqual(a, b Error) bool { 447 aPos := a.Position() 448 bPos := b.Position() 449 if aPos == token.NoPos || bPos == token.NoPos { 450 return a.Error() == b.Error() 451 } 452 return aPos.Filename() == bPos.Filename() && 453 aPos.Line() == bPos.Line() && 454 aPos.Column() == bPos.Column() && 455 comparePath(a.Path(), b.Path()) == 0 456 } 457 458 // An List implements the error interface. 459 func (p list) Error() string { 460 format, args := p.Msg() 461 return fmt.Sprintf(format, args...) 462 } 463 464 // Msg reports the unformatted error message for the first error, if any. 465 func (p list) Msg() (format string, args []interface{}) { 466 switch len(p) { 467 case 0: 468 return "no errors", nil 469 case 1: 470 return p[0].Msg() 471 } 472 return "%s (and %d more errors)", []interface{}{p[0], len(p) - 1} 473 } 474 475 // Position reports the primary position for the first error, if any. 476 func (p list) Position() token.Pos { 477 if len(p) == 0 { 478 return token.NoPos 479 } 480 return p[0].Position() 481 } 482 483 // InputPositions reports the input positions for the first error, if any. 484 func (p list) InputPositions() []token.Pos { 485 if len(p) == 0 { 486 return nil 487 } 488 return p[0].InputPositions() 489 } 490 491 // Path reports the path location of the first error, if any. 492 func (p list) Path() []string { 493 if len(p) == 0 { 494 return nil 495 } 496 return p[0].Path() 497 } 498 499 // Err returns an error equivalent to this error list. 500 // If the list is empty, Err returns nil. 501 func (p list) Err() error { 502 if len(p) == 0 { 503 return nil 504 } 505 return p 506 } 507 508 // A Config defines parameters for printing. 509 type Config struct { 510 // Format formats the given string and arguments and writes it to w. 511 // It is used for all printing. 512 Format func(w io.Writer, format string, args ...interface{}) 513 514 // Cwd is the current working directory. Filename positions are taken 515 // relative to this path. 516 Cwd string 517 518 // ToSlash sets whether to use Unix paths. Mostly used for testing. 519 ToSlash bool 520 } 521 522 // Print is a utility function that prints a list of errors to w, 523 // one error per line, if the err parameter is an List. Otherwise 524 // it prints the err string. 525 func Print(w io.Writer, err error, cfg *Config) { 526 if cfg == nil { 527 cfg = &Config{} 528 } 529 for _, e := range list(Errors(err)).sanitize() { 530 printError(w, e, cfg) 531 } 532 } 533 534 // Details is a convenience wrapper for Print to return the error text as a 535 // string. 536 func Details(err error, cfg *Config) string { 537 var b strings.Builder 538 Print(&b, err, cfg) 539 return b.String() 540 } 541 542 // String generates a short message from a given Error. 543 func String(err Error) string { 544 var b strings.Builder 545 writeErr(&b, err) 546 return b.String() 547 } 548 549 func writeErr(w io.Writer, err Error) { 550 if path := strings.Join(err.Path(), "."); path != "" { 551 _, _ = io.WriteString(w, path) 552 _, _ = io.WriteString(w, ": ") 553 } 554 555 for { 556 u := errors.Unwrap(err) 557 558 msg, args := err.Msg() 559 n, _ := fmt.Fprintf(w, msg, args...) 560 561 if u == nil { 562 break 563 } 564 565 if n > 0 { 566 _, _ = io.WriteString(w, ": ") 567 } 568 err, _ = u.(Error) 569 if err == nil { 570 fmt.Fprint(w, u) 571 break 572 } 573 } 574 } 575 576 func defaultFprintf(w io.Writer, format string, args ...interface{}) { 577 fmt.Fprintf(w, format, args...) 578 } 579 580 func printError(w io.Writer, err error, cfg *Config) { 581 if err == nil { 582 return 583 } 584 fprintf := cfg.Format 585 if fprintf == nil { 586 fprintf = defaultFprintf 587 } 588 589 positions := []string{} 590 for _, p := range Positions(err) { 591 pos := p.Position() 592 s := pos.Filename 593 if cfg.Cwd != "" { 594 if p, err := filepath.Rel(cfg.Cwd, s); err == nil { 595 s = p 596 // Some IDEs (e.g. VSCode) only recognize a path if it start 597 // with a dot. This also helps to distinguish between local 598 // files and builtin packages. 599 if !strings.HasPrefix(s, ".") { 600 s = fmt.Sprintf(".%s%s", string(filepath.Separator), s) 601 } 602 } 603 } 604 if cfg.ToSlash { 605 s = filepath.ToSlash(s) 606 } 607 if pos.IsValid() { 608 if s != "" { 609 s += ":" 610 } 611 s += fmt.Sprintf("%d:%d", pos.Line, pos.Column) 612 } 613 if s == "" { 614 s = "-" 615 } 616 positions = append(positions, s) 617 } 618 619 if e, ok := err.(Error); ok { 620 writeErr(w, e) 621 } else { 622 fprintf(w, "%v", err) 623 } 624 625 if len(positions) == 0 { 626 fprintf(w, "\n") 627 return 628 } 629 630 fprintf(w, ":\n") 631 for _, pos := range positions { 632 fprintf(w, " %s\n", pos) 633 } 634 }