file_error.go (9746B)
1 // Copyright 2022 The Hugo Authors. All rights reserved. 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 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable lfmtaw or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package herrors 15 16 import ( 17 "encoding/json" 18 "fmt" 19 "io" 20 "path/filepath" 21 22 "github.com/bep/godartsass" 23 "github.com/bep/golibsass/libsass/libsasserrors" 24 "github.com/gohugoio/hugo/common/paths" 25 "github.com/gohugoio/hugo/common/text" 26 "github.com/pelletier/go-toml/v2" 27 "github.com/spf13/afero" 28 "github.com/tdewolff/parse/v2" 29 30 "errors" 31 ) 32 33 // FileError represents an error when handling a file: Parsing a config file, 34 // execute a template etc. 35 type FileError interface { 36 error 37 38 // ErroContext holds some context information about the error. 39 ErrorContext() *ErrorContext 40 41 text.Positioner 42 43 // UpdatePosition updates the position of the error. 44 UpdatePosition(pos text.Position) FileError 45 46 // UpdateContent updates the error with a new ErrorContext from the content of the file. 47 UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError 48 } 49 50 // Unwrapper can unwrap errors created with fmt.Errorf. 51 type Unwrapper interface { 52 Unwrap() error 53 } 54 55 var ( 56 _ FileError = (*fileError)(nil) 57 _ Unwrapper = (*fileError)(nil) 58 ) 59 60 func (fe *fileError) UpdatePosition(pos text.Position) FileError { 61 oldFilename := fe.Position().Filename 62 if pos.Filename != "" && fe.fileType == "" { 63 _, fe.fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename)) 64 } 65 if pos.Filename == "" { 66 pos.Filename = oldFilename 67 } 68 fe.position = pos 69 return fe 70 } 71 72 func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError { 73 if linematcher == nil { 74 linematcher = SimpleLineMatcher 75 } 76 77 var ( 78 posle = fe.position 79 ectx *ErrorContext 80 ) 81 82 if posle.LineNumber <= 1 && posle.Offset > 0 { 83 // Try to locate the line number from the content if offset is set. 84 ectx = locateError(r, fe, func(m LineMatcher) int { 85 if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) { 86 lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber 87 m.Position = text.Position{LineNumber: lno} 88 return linematcher(m) 89 } 90 return -1 91 }) 92 } else { 93 ectx = locateError(r, fe, linematcher) 94 } 95 96 if ectx.ChromaLexer == "" { 97 if fe.fileType != "" { 98 ectx.ChromaLexer = chromaLexerFromType(fe.fileType) 99 } else { 100 ectx.ChromaLexer = chromaLexerFromFilename(fe.Position().Filename) 101 } 102 } 103 104 fe.errorContext = ectx 105 106 if ectx.Position.LineNumber > 0 { 107 fe.position.LineNumber = ectx.Position.LineNumber 108 } 109 110 if ectx.Position.ColumnNumber > 0 { 111 fe.position.ColumnNumber = ectx.Position.ColumnNumber 112 } 113 114 return fe 115 116 } 117 118 type fileError struct { 119 position text.Position 120 errorContext *ErrorContext 121 122 fileType string 123 124 cause error 125 } 126 127 func (e *fileError) ErrorContext() *ErrorContext { 128 return e.errorContext 129 } 130 131 // Position returns the text position of this error. 132 func (e fileError) Position() text.Position { 133 return e.position 134 } 135 136 func (e *fileError) Error() string { 137 return fmt.Sprintf("%s: %s", e.position, e.causeString()) 138 } 139 140 func (e *fileError) causeString() string { 141 if e.cause == nil { 142 return "" 143 } 144 switch v := e.cause.(type) { 145 // Avoid repeating the file info in the error message. 146 case godartsass.SassError: 147 return v.Message 148 case libsasserrors.Error: 149 return v.Message 150 default: 151 return v.Error() 152 } 153 } 154 155 func (e *fileError) Unwrap() error { 156 return e.cause 157 } 158 159 // NewFileError creates a new FileError that wraps err. 160 // It will try to extract the filename and line number from err. 161 func NewFileError(err error) FileError { 162 // Filetype is used to determine the Chroma lexer to use. 163 fileType, pos := extractFileTypePos(err) 164 return &fileError{cause: err, fileType: fileType, position: pos} 165 } 166 167 // NewFileErrorFromName creates a new FileError that wraps err. 168 // The value for name should identify the file, the best 169 // being the full filename to the file on disk. 170 func NewFileErrorFromName(err error, name string) FileError { 171 // Filetype is used to determine the Chroma lexer to use. 172 fileType, pos := extractFileTypePos(err) 173 pos.Filename = name 174 if fileType == "" { 175 _, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name)) 176 } 177 178 return &fileError{cause: err, fileType: fileType, position: pos} 179 180 } 181 182 // NewFileErrorFromPos will use the filename and line number from pos to create a new FileError, wrapping err. 183 func NewFileErrorFromPos(err error, pos text.Position) FileError { 184 // Filetype is used to determine the Chroma lexer to use. 185 fileType, _ := extractFileTypePos(err) 186 if fileType == "" { 187 _, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename)) 188 } 189 return &fileError{cause: err, fileType: fileType, position: pos} 190 191 } 192 193 func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError { 194 fe := NewFileError(err) 195 pos := fe.Position() 196 if pos.Filename == "" { 197 return fe 198 } 199 200 f, realFilename, err2 := openFile(pos.Filename, fs) 201 if err2 != nil { 202 return fe 203 } 204 205 pos.Filename = realFilename 206 defer f.Close() 207 return fe.UpdateContent(f, linematcher) 208 } 209 210 func NewFileErrorFromFileInPos(err error, pos text.Position, fs afero.Fs, linematcher LineMatcherFn) FileError { 211 if err == nil { 212 panic("err is nil") 213 } 214 f, realFilename, err2 := openFile(pos.Filename, fs) 215 if err2 != nil { 216 return NewFileErrorFromPos(err, pos) 217 } 218 pos.Filename = realFilename 219 defer f.Close() 220 return NewFileErrorFromPos(err, pos).UpdateContent(f, linematcher) 221 } 222 223 // NewFileErrorFromFile is a convenience method to create a new FileError from a file. 224 func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher LineMatcherFn) FileError { 225 if err == nil { 226 panic("err is nil") 227 } 228 f, realFilename, err2 := openFile(filename, fs) 229 if err2 != nil { 230 return NewFileErrorFromName(err, realFilename) 231 } 232 defer f.Close() 233 return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher) 234 } 235 236 func openFile(filename string, fs afero.Fs) (afero.File, string, error) { 237 realFilename := filename 238 239 // We want the most specific filename possible in the error message. 240 fi, err2 := fs.Stat(filename) 241 if err2 == nil { 242 if s, ok := fi.(interface { 243 Filename() string 244 }); ok { 245 realFilename = s.Filename() 246 } 247 248 } 249 250 f, err2 := fs.Open(filename) 251 if err2 != nil { 252 return nil, realFilename, err2 253 } 254 255 return f, realFilename, nil 256 } 257 258 // Cause returns the underlying error or itself if it does not implement Unwrap. 259 func Cause(err error) error { 260 if u := errors.Unwrap(err); u != nil { 261 return u 262 } 263 return err 264 } 265 266 func extractFileTypePos(err error) (string, text.Position) { 267 err = Cause(err) 268 269 var fileType string 270 271 // LibSass, DartSass 272 if pos := extractPosition(err); pos.LineNumber > 0 || pos.Offset > 0 { 273 _, fileType = paths.FileAndExtNoDelimiter(pos.Filename) 274 return fileType, pos 275 } 276 277 // Default to line 1 col 1 if we don't find any better. 278 pos := text.Position{ 279 Offset: -1, 280 LineNumber: 1, 281 ColumnNumber: 1, 282 } 283 284 // JSON errors. 285 offset, typ := extractOffsetAndType(err) 286 if fileType == "" { 287 fileType = typ 288 } 289 290 if offset >= 0 { 291 pos.Offset = offset 292 } 293 294 // The error type from the minifier contains line number and column number. 295 if line, col := exctractLineNumberAndColumnNumber(err); line >= 0 { 296 pos.LineNumber = line 297 pos.ColumnNumber = col 298 return fileType, pos 299 } 300 301 // Look in the error message for the line number. 302 for _, handle := range lineNumberExtractors { 303 lno, col := handle(err) 304 if lno > 0 { 305 pos.ColumnNumber = col 306 pos.LineNumber = lno 307 break 308 } 309 } 310 311 if fileType == "" && pos.Filename != "" { 312 _, fileType = paths.FileAndExtNoDelimiter(pos.Filename) 313 } 314 315 return fileType, pos 316 } 317 318 // UnwrapFileError tries to unwrap a FileError from err. 319 // It returns nil if this is not possible. 320 func UnwrapFileError(err error) FileError { 321 for err != nil { 322 switch v := err.(type) { 323 case FileError: 324 return v 325 default: 326 err = errors.Unwrap(err) 327 } 328 } 329 return nil 330 } 331 332 // UnwrapFileErrors tries to unwrap all FileError. 333 func UnwrapFileErrors(err error) []FileError { 334 var errs []FileError 335 for err != nil { 336 if v, ok := err.(FileError); ok { 337 errs = append(errs, v) 338 } 339 err = errors.Unwrap(err) 340 } 341 return errs 342 } 343 344 // UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext. 345 func UnwrapFileErrorsWithErrorContext(err error) []FileError { 346 var errs []FileError 347 for err != nil { 348 if v, ok := err.(FileError); ok && v.ErrorContext() != nil { 349 errs = append(errs, v) 350 } 351 err = errors.Unwrap(err) 352 } 353 return errs 354 } 355 356 func extractOffsetAndType(e error) (int, string) { 357 switch v := e.(type) { 358 case *json.UnmarshalTypeError: 359 return int(v.Offset), "json" 360 case *json.SyntaxError: 361 return int(v.Offset), "json" 362 default: 363 return -1, "" 364 } 365 } 366 367 func exctractLineNumberAndColumnNumber(e error) (int, int) { 368 switch v := e.(type) { 369 case *parse.Error: 370 return v.Line, v.Column 371 case *toml.DecodeError: 372 return v.Position() 373 374 } 375 376 return -1, -1 377 } 378 379 func extractPosition(e error) (pos text.Position) { 380 switch v := e.(type) { 381 case godartsass.SassError: 382 span := v.Span 383 start := span.Start 384 filename, _ := paths.UrlToFilename(span.Url) 385 pos.Filename = filename 386 pos.Offset = start.Offset 387 pos.ColumnNumber = start.Column 388 case libsasserrors.Error: 389 pos.Filename = v.File 390 pos.LineNumber = v.Line 391 pos.ColumnNumber = v.Column 392 } 393 return 394 }