github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/common/herrors/error_locator.go (about)

     1  // Copyright 2018 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 law 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 contains common Hugo errors and error related utilities.
    15  package herrors
    16  
    17  import (
    18  	"io"
    19  	"io/ioutil"
    20  	"path/filepath"
    21  	"strings"
    22  
    23  	"github.com/gohugoio/hugo/common/text"
    24  
    25  	"github.com/spf13/afero"
    26  )
    27  
    28  // LineMatcher contains the elements used to match an error to a line
    29  type LineMatcher struct {
    30  	Position text.Position
    31  	Error    error
    32  
    33  	LineNumber int
    34  	Offset     int
    35  	Line       string
    36  }
    37  
    38  // LineMatcherFn is used to match a line with an error.
    39  type LineMatcherFn func(m LineMatcher) bool
    40  
    41  // SimpleLineMatcher simply matches by line number.
    42  var SimpleLineMatcher = func(m LineMatcher) bool {
    43  	return m.Position.LineNumber == m.LineNumber
    44  }
    45  
    46  var _ text.Positioner = ErrorContext{}
    47  
    48  // ErrorContext contains contextual information about an error. This will
    49  // typically be the lines surrounding some problem in a file.
    50  type ErrorContext struct {
    51  
    52  	// If a match will contain the matched line and up to 2 lines before and after.
    53  	// Will be empty if no match.
    54  	Lines []string
    55  
    56  	// The position of the error in the Lines above. 0 based.
    57  	LinesPos int
    58  
    59  	position text.Position
    60  
    61  	// The lexer to use for syntax highlighting.
    62  	// https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages
    63  	ChromaLexer string
    64  }
    65  
    66  // Position returns the text position of this error.
    67  func (e ErrorContext) Position() text.Position {
    68  	return e.position
    69  }
    70  
    71  var _ causer = (*ErrorWithFileContext)(nil)
    72  
    73  // ErrorWithFileContext is an error with some additional file context related
    74  // to that error.
    75  type ErrorWithFileContext struct {
    76  	cause error
    77  	ErrorContext
    78  }
    79  
    80  func (e *ErrorWithFileContext) Error() string {
    81  	pos := e.Position()
    82  	if pos.IsValid() {
    83  		return pos.String() + ": " + e.cause.Error()
    84  	}
    85  	return e.cause.Error()
    86  }
    87  
    88  func (e *ErrorWithFileContext) Cause() error {
    89  	return e.cause
    90  }
    91  
    92  // WithFileContextForFile will try to add a file context with lines matching the given matcher.
    93  // If no match could be found, the original error is returned with false as the second return value.
    94  func WithFileContextForFile(e error, realFilename, filename string, fs afero.Fs, matcher LineMatcherFn) (error, bool) {
    95  	f, err := fs.Open(filename)
    96  	if err != nil {
    97  		return e, false
    98  	}
    99  	defer f.Close()
   100  	return WithFileContext(e, realFilename, f, matcher)
   101  }
   102  
   103  // WithFileContextForFileDefault tries to add file context using the default line matcher.
   104  func WithFileContextForFileDefault(err error, filename string, fs afero.Fs) error {
   105  	err, _ = WithFileContextForFile(
   106  		err,
   107  		filename,
   108  		filename,
   109  		fs,
   110  		SimpleLineMatcher)
   111  	return err
   112  }
   113  
   114  // WithFileContextForFile will try to add a file context with lines matching the given matcher.
   115  // If no match could be found, the original error is returned with false as the second return value.
   116  func WithFileContext(e error, realFilename string, r io.Reader, matcher LineMatcherFn) (error, bool) {
   117  	if e == nil {
   118  		panic("error missing")
   119  	}
   120  	le := UnwrapFileError(e)
   121  
   122  	if le == nil {
   123  		var ok bool
   124  		if le, ok = ToFileError("", e).(FileError); !ok {
   125  			return e, false
   126  		}
   127  	}
   128  
   129  	var errCtx ErrorContext
   130  
   131  	posle := le.Position()
   132  
   133  	if posle.Offset != -1 {
   134  		errCtx = locateError(r, le, func(m LineMatcher) bool {
   135  			if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) {
   136  				lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber
   137  				m.Position = text.Position{LineNumber: lno}
   138  			}
   139  			return matcher(m)
   140  		})
   141  	} else {
   142  		errCtx = locateError(r, le, matcher)
   143  	}
   144  
   145  	pos := &errCtx.position
   146  
   147  	if pos.LineNumber == -1 {
   148  		return e, false
   149  	}
   150  
   151  	pos.Filename = realFilename
   152  
   153  	if le.Type() != "" {
   154  		errCtx.ChromaLexer = chromaLexerFromType(le.Type())
   155  	} else {
   156  		errCtx.ChromaLexer = chromaLexerFromFilename(realFilename)
   157  	}
   158  
   159  	return &ErrorWithFileContext{cause: e, ErrorContext: errCtx}, true
   160  }
   161  
   162  // UnwrapErrorWithFileContext tries to unwrap an ErrorWithFileContext from err.
   163  // It returns nil if this is not possible.
   164  func UnwrapErrorWithFileContext(err error) *ErrorWithFileContext {
   165  	for err != nil {
   166  		switch v := err.(type) {
   167  		case *ErrorWithFileContext:
   168  			return v
   169  		case causer:
   170  			err = v.Cause()
   171  		default:
   172  			return nil
   173  		}
   174  	}
   175  	return nil
   176  }
   177  
   178  func chromaLexerFromType(fileType string) string {
   179  	switch fileType {
   180  	case "html", "htm":
   181  		return "go-html-template"
   182  	}
   183  	return fileType
   184  }
   185  
   186  func extNoDelimiter(filename string) string {
   187  	return strings.TrimPrefix(filepath.Ext(filename), ".")
   188  }
   189  
   190  func chromaLexerFromFilename(filename string) string {
   191  	if strings.Contains(filename, "layouts") {
   192  		return "go-html-template"
   193  	}
   194  
   195  	ext := extNoDelimiter(filename)
   196  	return chromaLexerFromType(ext)
   197  }
   198  
   199  func locateErrorInString(src string, matcher LineMatcherFn) ErrorContext {
   200  	return locateError(strings.NewReader(src), &fileError{}, matcher)
   201  }
   202  
   203  func locateError(r io.Reader, le FileError, matches LineMatcherFn) ErrorContext {
   204  	if le == nil {
   205  		panic("must provide an error")
   206  	}
   207  
   208  	errCtx := ErrorContext{position: text.Position{LineNumber: -1, ColumnNumber: 1, Offset: -1}, LinesPos: -1}
   209  
   210  	b, err := ioutil.ReadAll(r)
   211  	if err != nil {
   212  		return errCtx
   213  	}
   214  
   215  	pos := &errCtx.position
   216  	lepos := le.Position()
   217  
   218  	lines := strings.Split(string(b), "\n")
   219  
   220  	if lepos.ColumnNumber >= 0 {
   221  		pos.ColumnNumber = lepos.ColumnNumber
   222  	}
   223  
   224  	lineNo := 0
   225  	posBytes := 0
   226  
   227  	for li, line := range lines {
   228  		lineNo = li + 1
   229  		m := LineMatcher{
   230  			Position:   le.Position(),
   231  			Error:      le,
   232  			LineNumber: lineNo,
   233  			Offset:     posBytes,
   234  			Line:       line,
   235  		}
   236  		if errCtx.LinesPos == -1 && matches(m) {
   237  			pos.LineNumber = lineNo
   238  			break
   239  		}
   240  
   241  		posBytes += len(line)
   242  	}
   243  
   244  	if pos.LineNumber != -1 {
   245  		low := pos.LineNumber - 3
   246  		if low < 0 {
   247  			low = 0
   248  		}
   249  
   250  		if pos.LineNumber > 2 {
   251  			errCtx.LinesPos = 2
   252  		} else {
   253  			errCtx.LinesPos = pos.LineNumber - 1
   254  		}
   255  
   256  		high := pos.LineNumber + 2
   257  		if high > len(lines) {
   258  			high = len(lines)
   259  		}
   260  
   261  		errCtx.Lines = lines[low:high]
   262  
   263  	}
   264  
   265  	return errCtx
   266  }