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  }