...

Source file src/cuelang.org/go/cue/errors/errors.go

Documentation: cuelang.org/go/cue/errors

     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 "cuelang.org/go/cue/errors"
    21  
    22  import (
    23  	"cmp"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"path/filepath"
    28  	"slices"
    29  	"sort"
    30  	"strings"
    31  
    32  	"cuelang.org/go/cue/token"
    33  )
    34  
    35  // New is a convenience wrapper for errors.New in the core library.
    36  // It does not return a CUE error.
    37  func New(msg string) error {
    38  	return errors.New(msg)
    39  }
    40  
    41  // Unwrap returns the result of calling the Unwrap method on err, if err
    42  // implements Unwrap. Otherwise, Unwrap returns nil.
    43  func Unwrap(err error) error {
    44  	return errors.Unwrap(err)
    45  }
    46  
    47  // Is reports whether any error in err's chain matches target.
    48  //
    49  // An error is considered to match a target if it is equal to that target or if
    50  // it implements a method Is(error) bool such that Is(target) returns true.
    51  func Is(err, target error) bool {
    52  	return errors.Is(err, target)
    53  }
    54  
    55  // As finds the first error in err's chain that matches the type to which target
    56  // points, and if so, sets the target to its value and returns true. An error
    57  // matches a type if it is assignable to the target type, or if it has a method
    58  // As(interface{}) bool such that As(target) returns true. As will panic if
    59  // target is not a non-nil pointer to a type which implements error or is of
    60  // interface type.
    61  //
    62  // The As method should set the target to its value and return true if err
    63  // matches the type to which target points.
    64  func As(err error, target interface{}) bool {
    65  	return errors.As(err, target)
    66  }
    67  
    68  // A Message implements the error interface as well as Message to allow
    69  // internationalized messages. A Message is typically used as an embedding
    70  // in a CUE message.
    71  type Message struct {
    72  	format string
    73  	args   []interface{}
    74  }
    75  
    76  // NewMessagef creates an error message for human consumption. The arguments
    77  // are for later consumption, allowing the message to be localized at a later
    78  // time. The passed argument list should not be modified.
    79  func NewMessagef(format string, args ...interface{}) Message {
    80  	if false {
    81  		// Let go vet know that we're expecting printf-like arguments.
    82  		_ = fmt.Sprintf(format, args...)
    83  	}
    84  	return Message{format: format, args: args}
    85  }
    86  
    87  // NewMessage creates an error message for human consumption.
    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 := Path(e.main); 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 x := a.(type) {
   278  	case nil:
   279  		return b
   280  	case list:
   281  		return appendToList(x, b)
   282  	}
   283  	// Preserve order of errors.
   284  	list := appendToList(nil, a)
   285  	list = appendToList(list, b)
   286  	return list
   287  }
   288  
   289  // Errors reports the individual errors associated with an error, which is
   290  // the error itself if there is only one or, if the underlying type is List,
   291  // its individual elements. If the given error is not an Error, it will be
   292  // promoted to one.
   293  func Errors(err error) []Error {
   294  	if err == nil {
   295  		return nil
   296  	}
   297  	var listErr list
   298  	var errorErr Error
   299  	switch {
   300  	case As(err, &listErr):
   301  		return listErr
   302  	case As(err, &errorErr):
   303  		return []Error{errorErr}
   304  	default:
   305  		return []Error{Promote(err, "")}
   306  	}
   307  }
   308  
   309  func appendToList(a list, err Error) list {
   310  	switch x := err.(type) {
   311  	case nil:
   312  		return a
   313  	case list:
   314  		if a == nil {
   315  			return x
   316  		}
   317  		return append(a, x...)
   318  	default:
   319  		return append(a, err)
   320  	}
   321  }
   322  
   323  // list is a list of Errors.
   324  // The zero value for an list is an empty list ready to use.
   325  type list []Error
   326  
   327  func (p list) Is(target error) bool {
   328  	for _, e := range p {
   329  		if errors.Is(e, target) {
   330  			return true
   331  		}
   332  	}
   333  	return false
   334  }
   335  
   336  func (p list) As(target interface{}) bool {
   337  	for _, e := range p {
   338  		if errors.As(e, target) {
   339  			return true
   340  		}
   341  	}
   342  	return false
   343  }
   344  
   345  // AddNewf adds an Error with given position and error message to an List.
   346  func (p *list) AddNewf(pos token.Pos, msg string, args ...interface{}) {
   347  	err := &posError{pos: pos, Message: Message{format: msg, args: args}}
   348  	*p = append(*p, err)
   349  }
   350  
   351  // Add adds an Error with given position and error message to an List.
   352  func (p *list) Add(err Error) {
   353  	*p = appendToList(*p, err)
   354  }
   355  
   356  // Reset resets an List to no errors.
   357  func (p *list) Reset() { *p = (*p)[:0] }
   358  
   359  // List implements the sort Interface.
   360  func (p list) Len() int      { return len(p) }
   361  func (p list) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
   362  
   363  func (p list) Less(i, j int) bool {
   364  	if c := comparePos(p[i].Position(), p[j].Position()); c != 0 {
   365  		return c == -1
   366  	}
   367  	// Note that it is not sufficient to simply compare file offsets because
   368  	// the offsets do not reflect modified line information (through //line
   369  	// comments).
   370  
   371  	if !equalPath(p[i].Path(), p[j].Path()) {
   372  		return lessPath(p[i].Path(), p[j].Path())
   373  	}
   374  	return p[i].Error() < p[j].Error()
   375  }
   376  
   377  func comparePos(a, b token.Pos) int {
   378  	if a.Filename() != b.Filename() {
   379  		return cmp.Compare(a.Filename(), b.Filename())
   380  	}
   381  	if a.Line() != b.Line() {
   382  		return cmp.Compare(a.Line(), b.Line())
   383  	}
   384  	if a.Column() != b.Column() {
   385  		return cmp.Compare(a.Column(), b.Column())
   386  	}
   387  	return 0
   388  }
   389  
   390  func lessPath(a, b []string) bool {
   391  	for i, x := range a {
   392  		if i >= len(b) {
   393  			return false
   394  		}
   395  		if x != b[i] {
   396  			return x < b[i]
   397  		}
   398  	}
   399  	return len(a) < len(b)
   400  }
   401  
   402  func equalPath(a, b []string) bool {
   403  	if len(a) != len(b) {
   404  		return false
   405  	}
   406  	for i, x := range a {
   407  		if x != b[i] {
   408  			return false
   409  		}
   410  	}
   411  	return true
   412  }
   413  
   414  // Sanitize sorts multiple errors and removes duplicates on a best effort basis.
   415  // If err represents a single or no error, it returns the error as is.
   416  func Sanitize(err Error) Error {
   417  	if err == nil {
   418  		return nil
   419  	}
   420  	if l, ok := err.(list); ok {
   421  		a := l.sanitize()
   422  		if len(a) == 1 {
   423  			return a[0]
   424  		}
   425  		return a
   426  	}
   427  	return err
   428  }
   429  
   430  func (p list) sanitize() list {
   431  	if p == nil {
   432  		return p
   433  	}
   434  	a := make(list, len(p))
   435  	copy(a, p)
   436  	a.RemoveMultiples()
   437  	return a
   438  }
   439  
   440  // Sort sorts an List. *posError entries are sorted by position,
   441  // other errors are sorted by error message, and before any *posError
   442  // entry.
   443  func (p list) Sort() {
   444  	sort.Sort(p)
   445  }
   446  
   447  // RemoveMultiples sorts an List and removes all but the first error per line.
   448  func (p *list) RemoveMultiples() {
   449  	p.Sort()
   450  	var last Error
   451  	i := 0
   452  	for _, e := range *p {
   453  		if last == nil || !approximateEqual(last, e) {
   454  			last = e
   455  			(*p)[i] = e
   456  			i++
   457  		}
   458  	}
   459  	(*p) = (*p)[0:i]
   460  }
   461  
   462  func approximateEqual(a, b Error) bool {
   463  	aPos := a.Position()
   464  	bPos := b.Position()
   465  	if aPos == token.NoPos || bPos == token.NoPos {
   466  		return a.Error() == b.Error()
   467  	}
   468  	return aPos.Filename() == bPos.Filename() &&
   469  		aPos.Line() == bPos.Line() &&
   470  		aPos.Column() == bPos.Column() &&
   471  		equalPath(a.Path(), b.Path())
   472  }
   473  
   474  // An List implements the error interface.
   475  func (p list) Error() string {
   476  	format, args := p.Msg()
   477  	return fmt.Sprintf(format, args...)
   478  }
   479  
   480  // Msg reports the unformatted error message for the first error, if any.
   481  func (p list) Msg() (format string, args []interface{}) {
   482  	switch len(p) {
   483  	case 0:
   484  		return "no errors", nil
   485  	case 1:
   486  		return p[0].Msg()
   487  	}
   488  	return "%s (and %d more errors)", []interface{}{p[0], len(p) - 1}
   489  }
   490  
   491  // Position reports the primary position for the first error, if any.
   492  func (p list) Position() token.Pos {
   493  	if len(p) == 0 {
   494  		return token.NoPos
   495  	}
   496  	return p[0].Position()
   497  }
   498  
   499  // InputPositions reports the input positions for the first error, if any.
   500  func (p list) InputPositions() []token.Pos {
   501  	if len(p) == 0 {
   502  		return nil
   503  	}
   504  	return p[0].InputPositions()
   505  }
   506  
   507  // Path reports the path location of the first error, if any.
   508  func (p list) Path() []string {
   509  	if len(p) == 0 {
   510  		return nil
   511  	}
   512  	return p[0].Path()
   513  }
   514  
   515  // Err returns an error equivalent to this error list.
   516  // If the list is empty, Err returns nil.
   517  func (p list) Err() error {
   518  	if len(p) == 0 {
   519  		return nil
   520  	}
   521  	return p
   522  }
   523  
   524  // A Config defines parameters for printing.
   525  type Config struct {
   526  	// Format formats the given string and arguments and writes it to w.
   527  	// It is used for all printing.
   528  	Format func(w io.Writer, format string, args ...interface{})
   529  
   530  	// Cwd is the current working directory. Filename positions are taken
   531  	// relative to this path.
   532  	Cwd string
   533  
   534  	// ToSlash sets whether to use Unix paths. Mostly used for testing.
   535  	ToSlash bool
   536  }
   537  
   538  // Print is a utility function that prints a list of errors to w,
   539  // one error per line, if the err parameter is an List. Otherwise
   540  // it prints the err string.
   541  func Print(w io.Writer, err error, cfg *Config) {
   542  	if cfg == nil {
   543  		cfg = &Config{}
   544  	}
   545  	for _, e := range list(Errors(err)).sanitize() {
   546  		printError(w, e, cfg)
   547  	}
   548  }
   549  
   550  // Details is a convenience wrapper for Print to return the error text as a
   551  // string.
   552  func Details(err error, cfg *Config) string {
   553  	var b strings.Builder
   554  	Print(&b, err, cfg)
   555  	return b.String()
   556  }
   557  
   558  // String generates a short message from a given Error.
   559  func String(err Error) string {
   560  	var b strings.Builder
   561  	writeErr(&b, err)
   562  	return b.String()
   563  }
   564  
   565  func writeErr(w io.Writer, err Error) {
   566  	if path := strings.Join(err.Path(), "."); path != "" {
   567  		_, _ = io.WriteString(w, path)
   568  		_, _ = io.WriteString(w, ": ")
   569  	}
   570  
   571  	for {
   572  		u := errors.Unwrap(err)
   573  
   574  		printed := false
   575  		msg, args := err.Msg()
   576  		s := fmt.Sprintf(msg, args...)
   577  		if s != "" || u == nil { // print at least something
   578  			_, _ = io.WriteString(w, s)
   579  			printed = true
   580  		}
   581  
   582  		if u == nil {
   583  			break
   584  		}
   585  
   586  		if printed {
   587  			_, _ = io.WriteString(w, ": ")
   588  		}
   589  		err, _ = u.(Error)
   590  		if err == nil {
   591  			fmt.Fprint(w, u)
   592  			break
   593  		}
   594  	}
   595  }
   596  
   597  func defaultFprintf(w io.Writer, format string, args ...interface{}) {
   598  	fmt.Fprintf(w, format, args...)
   599  }
   600  
   601  func printError(w io.Writer, err error, cfg *Config) {
   602  	if err == nil {
   603  		return
   604  	}
   605  	fprintf := cfg.Format
   606  	if fprintf == nil {
   607  		fprintf = defaultFprintf
   608  	}
   609  
   610  	positions := []string{}
   611  	for _, p := range Positions(err) {
   612  		pos := p.Position()
   613  		s := pos.Filename
   614  		if cfg.Cwd != "" {
   615  			if p, err := filepath.Rel(cfg.Cwd, s); err == nil {
   616  				s = p
   617  				// Some IDEs (e.g. VSCode) only recognize a path if it start
   618  				// with a dot. This also helps to distinguish between local
   619  				// files and builtin packages.
   620  				if !strings.HasPrefix(s, ".") {
   621  					s = fmt.Sprintf(".%s%s", string(filepath.Separator), s)
   622  				}
   623  			}
   624  		}
   625  		if cfg.ToSlash {
   626  			s = filepath.ToSlash(s)
   627  		}
   628  		if pos.IsValid() {
   629  			if s != "" {
   630  				s += ":"
   631  			}
   632  			s += fmt.Sprintf("%d:%d", pos.Line, pos.Column)
   633  		}
   634  		if s == "" {
   635  			s = "-"
   636  		}
   637  		positions = append(positions, s)
   638  	}
   639  
   640  	if e, ok := err.(Error); ok {
   641  		writeErr(w, e)
   642  	} else {
   643  		fprintf(w, "%v", err)
   644  	}
   645  
   646  	if len(positions) == 0 {
   647  		fprintf(w, "\n")
   648  		return
   649  	}
   650  
   651  	fprintf(w, ":\n")
   652  	for _, pos := range positions {
   653  		fprintf(w, "    %s\n", pos)
   654  	}
   655  }
   656  

View as plain text