hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 }