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 }