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