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