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  }