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  }