file_error.go (9746B)
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 }