github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/parse/asp/errors.go (about)

     1  package asp
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"cli"
    13  	"core"
    14  	"fs"
    15  )
    16  
    17  const (
    18  	// ANSI formatting codes
    19  	reset     = "\033[0m"
    20  	boldRed   = "\033[31;1m"
    21  	boldWhite = "\033[37;1m"
    22  	red       = "\033[31m"
    23  	yellow    = "\033[33m"
    24  	white     = "\033[37m"
    25  	grey      = "\033[30m"
    26  )
    27  
    28  // An errorStack is an error that carries an internal stack trace.
    29  type errorStack struct {
    30  	// From top down, i.e. Stack[0] is the innermost function in the call stack.
    31  	Stack []Position
    32  	// Readers that correspond to each level in the stack trace.
    33  	// Each may be nil but this will always have the same length as Stack.
    34  	Readers []io.ReadSeeker
    35  	// The original error that was encountered.
    36  	err error
    37  }
    38  
    39  // fail panics on lex/parse errors in a file.
    40  // For convenience we reuse errorStack although there is of course not really a call stack at this point.
    41  func fail(pos Position, message string, args ...interface{}) {
    42  	panic(AddStackFrame(pos, fmt.Errorf(message, args...)))
    43  }
    44  
    45  // AddStackFrame adds a new stack frame to the given errorStack, or wraps an existing error if not.
    46  func AddStackFrame(pos Position, err interface{}) error {
    47  	stack, ok := err.(*errorStack)
    48  	if !ok {
    49  		if e, ok := err.(error); ok {
    50  			stack = &errorStack{err: e}
    51  		} else {
    52  			stack = &errorStack{err: fmt.Errorf("%s", err)}
    53  		}
    54  	} else if n := len(stack.Stack) - 1; n > 0 && stack.Stack[n].Filename == pos.Filename && stack.Stack[n].Line == pos.Line {
    55  		return stack // Don't duplicate the same line multiple times. Often happens since one line can have multiple expressions.
    56  	}
    57  	stack.Stack = append(stack.Stack, pos)
    58  	stack.Readers = append(stack.Readers, nil)
    59  	return stack
    60  }
    61  
    62  // AddReader adds an io.Reader to an errStack, which will allow it to recover more information from that file.
    63  func AddReader(err error, r io.ReadSeeker) error {
    64  	if stack, ok := err.(*errorStack); ok {
    65  		stack.AddReader(r)
    66  	}
    67  	return err
    68  }
    69  
    70  // Error implements the builtin error interface.
    71  func (stack *errorStack) Error() string {
    72  	if len(stack.Stack) > 1 {
    73  		return stack.errorMessage() + "\n" + stack.stackTrace()
    74  	}
    75  	return stack.errorMessage()
    76  }
    77  
    78  // ShortError returns an abbreviated message with jsut what immediately went wrong.
    79  func (stack *errorStack) ShortError() string {
    80  	return stack.err.Error()
    81  }
    82  
    83  // stackTrace returns the lines of stacktrace from the error.
    84  func (stack *errorStack) stackTrace() string {
    85  	ret := make([]string, len(stack.Stack))
    86  	filenames := make([]string, len(stack.Stack))
    87  	lines := make([]string, len(stack.Stack))
    88  	cols := make([]string, len(stack.Stack))
    89  	for i, frame := range stack.Stack {
    90  		filenames[i] = frame.Filename
    91  		lines[i] = strconv.Itoa(frame.Line)
    92  		cols[i] = strconv.Itoa(frame.Column)
    93  	}
    94  	stack.equaliseLengths(filenames)
    95  	stack.equaliseLengths(lines)
    96  	stack.equaliseLengths(cols)
    97  	// Add final message & colours if appropriate
    98  	lastLine := 0
    99  	lastFile := ""
   100  	for i, frame := range stack.Stack {
   101  		if frame.Line == lastLine && frame.Filename == lastFile {
   102  			continue // Don't show the same line twice.
   103  		}
   104  		_, line, _ := stack.readLine(stack.Readers[i], frame.Line-1)
   105  		if line == "" {
   106  			line = "<source unavailable>"
   107  			if cli.StdErrIsATerminal {
   108  				line = grey + line + reset
   109  			}
   110  		}
   111  		s := fmt.Sprintf("%s:%s:%s:", filenames[i], lines[i], cols[i])
   112  		if !cli.StdErrIsATerminal {
   113  			ret[i] = fmt.Sprintf("%s   %s", s, line)
   114  		} else {
   115  			ret[i] = fmt.Sprintf("%s%s%s   %s", yellow, s, reset, line)
   116  		}
   117  		lastLine = frame.Line
   118  		lastFile = frame.Filename
   119  	}
   120  	msg := "Traceback:\n"
   121  	if cli.StdErrIsATerminal {
   122  		msg = boldWhite + msg + reset
   123  	}
   124  	return msg + strings.Join(ret, "\n")
   125  }
   126  
   127  // equaliseLengths left-pads the given strings so they are all of equal length.
   128  func (stack *errorStack) equaliseLengths(sl []string) {
   129  	max := 0
   130  	for _, s := range sl {
   131  		if len(s) > max {
   132  			max = len(s)
   133  		}
   134  	}
   135  	for i, s := range sl {
   136  		sl[i] = strings.Repeat(" ", max-len(s)) + s
   137  	}
   138  }
   139  
   140  // errorMessage returns the first part of the error message (i.e. the main message & file context)
   141  func (stack *errorStack) errorMessage() string {
   142  	frame := stack.Stack[0]
   143  	if before, line, after := stack.readLine(stack.Readers[0], frame.Line-1); line != "" || before != "" || after != "" {
   144  		charsBefore := frame.Column - 1
   145  		if charsBefore < 0 { // strings.Repeat panics if negative
   146  			charsBefore = 0
   147  		} else if charsBefore == len(line) {
   148  			line = line + "  "
   149  		} else if charsBefore > len(line) {
   150  			return stack.Error() // probably something's gone wrong and we're on totally the wrong line.
   151  		}
   152  		spaces := strings.Repeat(" ", charsBefore)
   153  		if !cli.StdErrIsATerminal {
   154  			return fmt.Sprintf("%s:%d:%d: error: %s\n%s\n%s\n%s^\n%s\n",
   155  				frame.Filename, frame.Line, frame.Column, stack.err, before, line, spaces, after)
   156  		}
   157  		// Add colour hints as well. It's a bit weird to add them here where we don't know
   158  		// how this is going to be printed, but not obvious how to solve well.
   159  		return fmt.Sprintf("%s%s%s:%s%d%s:%s%d%s: %serror:%s %s%s%s\n%s%s\n%s%s%s%c%s%s\n%s^\n%s%s%s\n",
   160  			boldWhite, frame.Filename, reset,
   161  			boldWhite, frame.Line, reset,
   162  			boldWhite, frame.Column, reset,
   163  			boldRed, reset,
   164  			boldWhite, stack.err, reset,
   165  			grey, before,
   166  			white, line[:charsBefore], red, line[charsBefore], white, line[charsBefore+1:],
   167  			spaces,
   168  			grey, after, reset,
   169  		)
   170  	}
   171  	return stack.err.Error()
   172  }
   173  
   174  // readLine reads a particular line of a reader plus some context.
   175  func (stack *errorStack) readLine(r io.ReadSeeker, line int) (string, string, string) {
   176  	// The reader for any level of the stack is allowed to be nil.
   177  	if r == nil {
   178  		return "", "", ""
   179  	}
   180  	r.Seek(0, io.SeekStart)
   181  	// This isn't 100% efficient but who cares really.
   182  	b, err := ioutil.ReadAll(r)
   183  	if err != nil {
   184  		return "", "", ""
   185  	}
   186  	lines := bytes.Split(b, []byte{'\n'})
   187  	if len(lines) <= line {
   188  		return "", "", ""
   189  	}
   190  	before := ""
   191  	if line > 0 {
   192  		before = string(lines[line-1])
   193  	}
   194  	after := ""
   195  	if line < len(lines)-1 {
   196  		after = string(lines[line+1])
   197  	}
   198  	return before, string(lines[line]), after
   199  }
   200  
   201  // AddReader adds an io.Reader into this error where appropriate.
   202  func (stack *errorStack) AddReader(r io.ReadSeeker) {
   203  	for i, r2 := range stack.Readers {
   204  		if r2 == nil {
   205  			fn := stack.Stack[i].Filename
   206  			if NameOfReader(r) == fn {
   207  				stack.Readers[i] = r
   208  			} else if f, err := os.Open(fn); err == nil {
   209  				// Maybe it's just a file on disk (e.g. via subinclude)
   210  				stack.Readers[i] = f
   211  				// If it was generated by a filegroup, it might match the in-repo source.
   212  				// In that case it's a little ugly to present the leading plz-out/gen.
   213  				if fn2 := strings.TrimPrefix(fn, core.GenDir+"/"); fn2 != fn && fs.IsSameFile(fn, fn2) {
   214  					stack.Stack[i].Filename = fn2
   215  				}
   216  			}
   217  		}
   218  	}
   219  }
   220  
   221  // A namedReader implements Name() on a Reader, allowing the lexer to automatically retrieve its name.
   222  // This is a bit awkward but unfortunately all we have when we try to access it is an io.Reader.
   223  type namedReader struct {
   224  	r    io.ReadSeeker
   225  	name string
   226  }
   227  
   228  // Read implements the io.Reader interface
   229  func (r *namedReader) Read(b []byte) (int, error) {
   230  	return r.r.Read(b)
   231  }
   232  
   233  // Name implements the internal namer interface
   234  func (r *namedReader) Name() string {
   235  	return r.name
   236  }
   237  
   238  // Seek implements the io.Seeker interface
   239  func (r *namedReader) Seek(offset int64, whence int) (int64, error) {
   240  	return r.r.Seek(offset, whence)
   241  }