github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/common/herrors/file_error.go (about)

     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  }