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 }