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  }