summaryrefslogtreecommitdiffstats
path: root/common
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-05-15 11:40:34 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-05-15 20:25:25 +0200
commitfc9f315d86e1fe51c3d1eec3b60680113b2e3aa6 (patch)
tree69e8ffc4d84e8f02e0e6f098c27cda9dd3bcc544 /common
parent4b189d8fd93d3fa326b31d451d5594c917e6c714 (diff)
Improve SASS errors
Fixes #9897
Diffstat (limited to 'common')
-rw-r--r--common/herrors/error_locator.go10
-rw-r--r--common/herrors/file_error.go78
-rw-r--r--common/herrors/file_error_test.go4
-rw-r--r--common/paths/url.go27
4 files changed, 113 insertions, 6 deletions
diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go
index 0f22f545f..18c21e51b 100644
--- a/common/herrors/error_locator.go
+++ b/common/herrors/error_locator.go
@@ -52,6 +52,16 @@ var NopLineMatcher = func(m LineMatcher) int {
return 1
}
+// OffsetMatcher is a line matcher that matches by offset.
+var OffsetMatcher = func(m LineMatcher) int {
+ if m.Offset+len(m.Line) >= m.Position.Offset {
+ // We found the line, but return 0 to signal that we want to determine
+ // the column from the error.
+ return 0
+ }
+ return -1
+}
+
// ErrorContext contains contextual information about an error. This will
// typically be the lines surrounding some problem in a file.
type ErrorContext struct {
diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go
index 85007a057..e6baaf6e3 100644
--- a/common/herrors/file_error.go
+++ b/common/herrors/file_error.go
@@ -19,6 +19,8 @@ import (
"io"
"path/filepath"
+ "github.com/bep/godartsass"
+ "github.com/bep/golibsass/libsass/libsasserrors"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/text"
"github.com/pelletier/go-toml/v2"
@@ -132,7 +134,22 @@ func (e fileError) Position() text.Position {
}
func (e *fileError) Error() string {
- return fmt.Sprintf("%s: %s", e.position, e.cause)
+ return fmt.Sprintf("%s: %s", e.position, e.causeString())
+}
+
+func (e *fileError) causeString() string {
+ if e.cause == nil {
+ return ""
+ }
+ switch v := e.cause.(type) {
+ // Avoid repeating the file info in the error message.
+ case godartsass.SassError:
+ return v.Message
+ case libsasserrors.Error:
+ return v.Message
+ default:
+ return v.Error()
+ }
}
func (e *fileError) Unwrap() error {
@@ -140,9 +157,17 @@ func (e *fileError) Unwrap() error {
}
// NewFileError creates a new FileError that wraps err.
+// It will try to extract the filename and line number from err.
+func NewFileError(err error) FileError {
+ // Filetype is used to determine the Chroma lexer to use.
+ fileType, pos := extractFileTypePos(err)
+ return &fileError{cause: err, fileType: fileType, position: pos}
+}
+
+// NewFileErrorFromName creates a new FileError that wraps err.
// The value for name should identify the file, the best
// being the full filename to the file on disk.
-func NewFileError(err error, name string) FileError {
+func NewFileErrorFromName(err error, name string) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err)
pos.Filename = name
@@ -165,6 +190,23 @@ func NewFileErrorFromPos(err error, pos text.Position) FileError {
}
+func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError {
+ fe := NewFileError(err)
+ pos := fe.Position()
+ if pos.Filename == "" {
+ return fe
+ }
+
+ f, realFilename, err2 := openFile(pos.Filename, fs)
+ if err2 != nil {
+ return fe
+ }
+
+ pos.Filename = realFilename
+ defer f.Close()
+ return fe.UpdateContent(f, linematcher)
+}
+
func NewFileErrorFromFileInPos(err error, pos text.Position, fs afero.Fs, linematcher LineMatcherFn) FileError {
if err == nil {
panic("err is nil")
@@ -185,10 +227,10 @@ func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher L
}
f, realFilename, err2 := openFile(filename, fs)
if err2 != nil {
- return NewFileError(err, realFilename)
+ return NewFileErrorFromName(err, realFilename)
}
defer f.Close()
- return NewFileError(err, realFilename).UpdateContent(f, linematcher)
+ return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
}
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
@@ -223,8 +265,15 @@ func Cause(err error) error {
func extractFileTypePos(err error) (string, text.Position) {
err = Cause(err)
+
var fileType string
+ // LibSass, DartSass
+ if pos := extractPosition(err); pos.LineNumber > 0 || pos.Offset > 0 {
+ _, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
+ return fileType, pos
+ }
+
// Default to line 1 col 1 if we don't find any better.
pos := text.Position{
Offset: -1,
@@ -259,6 +308,10 @@ func extractFileTypePos(err error) (string, text.Position) {
}
}
+ if fileType == "" && pos.Filename != "" {
+ _, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
+ }
+
return fileType, pos
}
@@ -322,3 +375,20 @@ func exctractLineNumberAndColumnNumber(e error) (int, int) {
return -1, -1
}
+
+func extractPosition(e error) (pos text.Position) {
+ switch v := e.(type) {
+ case godartsass.SassError:
+ span := v.Span
+ start := span.Start
+ filename, _ := paths.UrlToFilename(span.Url)
+ pos.Filename = filename
+ pos.Offset = start.Offset
+ pos.ColumnNumber = start.Column
+ case libsasserrors.Error:
+ pos.Filename = v.File
+ pos.LineNumber = v.Line
+ pos.ColumnNumber = v.Column
+ }
+ return
+}
diff --git a/common/herrors/file_error_test.go b/common/herrors/file_error_test.go
index 04baadf16..0b260a255 100644
--- a/common/herrors/file_error_test.go
+++ b/common/herrors/file_error_test.go
@@ -30,7 +30,7 @@ func TestNewFileError(t *testing.T) {
c := qt.New(t)
- fe := NewFileError(errors.New("bar"), "foo.html")
+ fe := NewFileErrorFromName(errors.New("bar"), "foo.html")
c.Assert(fe.Error(), qt.Equals, `"foo.html:1:1": bar`)
lines := ""
@@ -70,7 +70,7 @@ func TestNewFileErrorExtractFromMessage(t *testing.T) {
{errors.New(`execute of template failed: template: index.html:2:5: executing "index.html" at <partial "foo.html" .>: error calling partial: "/layouts/partials/foo.html:3:6": execute of template failed: template: partials/foo.html:3:6: executing "partials/foo.html" at <.ThisDoesNotExist>: can't evaluate field ThisDoesNotExist in type *hugolib.pageStat`), 0, 2, 5},
} {
- got := NewFileError(test.in, "test.txt")
+ got := NewFileErrorFromName(test.in, "test.txt")
errMsg := qt.Commentf("[%d][%T]", i, got)
diff --git a/common/paths/url.go b/common/paths/url.go
index 193b0cd0e..c538d8f2c 100644
--- a/common/paths/url.go
+++ b/common/paths/url.go
@@ -17,6 +17,7 @@ import (
"fmt"
"net/url"
"path"
+ "path/filepath"
"strings"
)
@@ -152,3 +153,29 @@ func Uglify(in string) string {
// /section/name.html -> /section/name.html
return path.Clean(in)
}
+
+// UrlToFilename converts the URL s to a filename.
+// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned.
+func UrlToFilename(s string) (string, bool) {
+ u, err := url.ParseRequestURI(s)
+
+ if err != nil {
+ return filepath.FromSlash(s), false
+ }
+
+ p := u.Path
+
+ if p == "" {
+ p, _ = url.QueryUnescape(u.Opaque)
+ return filepath.FromSlash(p), true
+ }
+
+ p = filepath.FromSlash(p)
+
+ if u.Host != "" {
+ // C:\data\file.txt
+ p = strings.ToUpper(u.Host) + ":" + p
+ }
+
+ return p, true
+}