diff options
Diffstat (limited to 'common/herrors/file_error.go')
-rw-r--r-- | common/herrors/file_error.go | 253 |
1 files changed, 200 insertions, 53 deletions
diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go index 1cb31ff9f..abd36cfbc 100644 --- a/common/herrors/file_error.go +++ b/common/herrors/file_error.go @@ -1,11 +1,11 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2022 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // -// Unless required by applicable law or agreed to in writing, software +// Unless required by applicable lfmtaw or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and @@ -15,59 +15,217 @@ package herrors import ( "encoding/json" + "fmt" + "io" + "path/filepath" + "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/text" + "github.com/pelletier/go-toml/v2" + "github.com/spf13/afero" + "github.com/tdewolff/parse/v2" - "github.com/pkg/errors" + "errors" ) -var _ causer = (*fileError)(nil) - // FileError represents an error when handling a file: Parsing a config file, // execute a template etc. type FileError interface { error + // ErroContext holds some context information about the error. + ErrorContext() *ErrorContext + text.Positioner - // A string identifying the type of file, e.g. JSON, TOML, markdown etc. - Type() string + // UpdatePosition updates the position of the error. + UpdatePosition(pos text.Position) FileError + + // UpdateContent updates the error with a new ErrorContext from the content of the file. + UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError +} + +// Unwrapper can unwrap errors created with fmt.Errorf. +type Unwrapper interface { + Unwrap() error } -var _ FileError = (*fileError)(nil) +var ( + _ FileError = (*fileError)(nil) + _ Unwrapper = (*fileError)(nil) +) + +func (fe *fileError) UpdatePosition(pos text.Position) FileError { + oldFilename := fe.Position().Filename + if pos.Filename != "" && fe.fileType == "" { + _, fe.fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename)) + } + if pos.Filename == "" { + pos.Filename = oldFilename + } + fe.position = pos + return fe +} + +func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError { + if linematcher == nil { + linematcher = SimpleLineMatcher + } + + var ( + contentPos text.Position + posle = fe.position + errorContext *ErrorContext + ) + + if posle.LineNumber <= 1 && posle.Offset > 0 { + // Try to locate the line number from the content if offset is set. + errorContext, contentPos = locateError(r, fe, func(m LineMatcher) bool { + if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) { + lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber + m.Position = text.Position{LineNumber: lno} + return linematcher(m) + } + return false + }) + } else { + errorContext, contentPos = locateError(r, fe, linematcher) + } + + if errorContext.ChromaLexer == "" { + if fe.fileType != "" { + errorContext.ChromaLexer = chromaLexerFromType(fe.fileType) + } else { + errorContext.ChromaLexer = chromaLexerFromFilename(fe.Position().Filename) + } + } + + fe.errorContext = errorContext + + if contentPos.LineNumber > 0 { + fe.position.LineNumber = contentPos.LineNumber + } + + return fe + +} type fileError struct { - position text.Position + position text.Position + errorContext *ErrorContext fileType string cause error } +type fileErrorWithErrorContext struct { + *fileError +} + +func (e *fileError) ErrorContext() *ErrorContext { + return e.errorContext +} + // Position returns the text position of this error. func (e fileError) Position() text.Position { return e.position } -func (e *fileError) Type() string { - return e.fileType +func (e *fileError) Error() string { + return fmt.Sprintf("%s: %s", e.position, e.cause) } -func (e *fileError) Error() string { - if e.cause == nil { - return "" +func (e *fileError) Unwrap() error { + return e.cause +} + +// NewFileError creates a new FileError that wraps err. +// The value for name should identify the file, the best +// being the full filename to the file on disk. +func NewFileError(name string, err error) FileError { + if err == nil { + panic("err is nil") + } + + // Filetype is used to determine the Chroma lexer to use. + fileType, pos := extractFileTypePos(err) + pos.Filename = name + if fileType == "" { + _, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name)) } - return e.cause.Error() + + if pos.LineNumber < 0 { + panic(fmt.Sprintf("invalid line number: %d", pos.LineNumber)) + } + + return &fileError{cause: err, fileType: fileType, position: pos} + } -func (f *fileError) Cause() error { - return f.cause +// NewFileErrorFromFile is a convenience method to create a new FileError from a file. +func NewFileErrorFromFile(err error, filename, realFilename string, fs afero.Fs, linematcher LineMatcherFn) FileError { + if err == nil { + panic("err is nil") + } + if linematcher == nil { + linematcher = SimpleLineMatcher + } + f, err2 := fs.Open(filename) + if err2 != nil { + return NewFileError(realFilename, err) + } + defer f.Close() + return NewFileError(realFilename, err).UpdateContent(f, linematcher) } -// NewFileError creates a new FileError. -func NewFileError(fileType string, offset, lineNumber, columnNumber int, err error) FileError { - pos := text.Position{Offset: offset, LineNumber: lineNumber, ColumnNumber: columnNumber} - return &fileError{cause: err, fileType: fileType, position: pos} +// Cause returns the underlying error or itself if it does not implement Unwrap. +func Cause(err error) error { + if u := errors.Unwrap(err); u != nil { + return u + } + return err +} + +func extractFileTypePos(err error) (string, text.Position) { + err = Cause(err) + var fileType string + + // Fall back to line/col 1:1 if we cannot find any better information. + pos := text.Position{ + Offset: -1, + LineNumber: 1, + ColumnNumber: 1, + } + + // JSON errors. + offset, typ := extractOffsetAndType(err) + if fileType == "" { + fileType = typ + } + + if offset >= 0 { + pos.Offset = offset + } + + // The error type from the minifier contains line number and column number. + if line, col := exctractLineNumberAndColumnNumber(err); line >= 0 { + pos.LineNumber = line + pos.ColumnNumber = col + return fileType, pos + } + + // Look in the error message for the line number. + for _, handle := range lineNumberExtractors { + lno, col := handle(err) + if lno > 0 { + pos.ColumnNumber = col + pos.LineNumber = lno + break + } + } + + return fileType, pos } // UnwrapFileError tries to unwrap a FileError from err. @@ -77,49 +235,26 @@ func UnwrapFileError(err error) FileError { switch v := err.(type) { case FileError: return v - case causer: - err = v.Cause() default: - return nil + err = errors.Unwrap(err) } } return nil } -// ToFileErrorWithOffset will return a new FileError with a line number -// with the given offset from the original. -func ToFileErrorWithOffset(fe FileError, offset int) FileError { - pos := fe.Position() - return ToFileErrorWithLineNumber(fe, pos.LineNumber+offset) -} - -// ToFileErrorWithOffset will return a new FileError with the given line number. -func ToFileErrorWithLineNumber(fe FileError, lineNumber int) FileError { - pos := fe.Position() - pos.LineNumber = lineNumber - return &fileError{cause: fe, fileType: fe.Type(), position: pos} -} - -// ToFileError will convert the given error to an error supporting -// the FileError interface. -func ToFileError(fileType string, err error) FileError { - for _, handle := range lineNumberExtractors { - lno, col := handle(err) - offset, typ := extractOffsetAndType(err) - if fileType == "" { - fileType = typ - } - - if lno > 0 || offset != -1 { - return NewFileError(fileType, offset, lno, col, err) +// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext. +func UnwrapFileErrorsWithErrorContext(err error) []FileError { + var errs []FileError + for err != nil { + if v, ok := err.(FileError); ok && v.ErrorContext() != nil { + errs = append(errs, v) } + err = errors.Unwrap(err) } - // Fall back to the pointing to line number 1. - return NewFileError(fileType, -1, 1, 1, err) + return errs } func extractOffsetAndType(e error) (int, string) { - e = errors.Cause(e) switch v := e.(type) { case *json.UnmarshalTypeError: return int(v.Offset), "json" @@ -129,3 +264,15 @@ func extractOffsetAndType(e error) (int, string) { return -1, "" } } + +func exctractLineNumberAndColumnNumber(e error) (int, int) { + switch v := e.(type) { + case *parse.Error: + return v.Line, v.Column + case *toml.DecodeError: + return v.Position() + + } + + return -1, -1 +} |