mirror of
https://github.com/Dadido3/go-typst.git
synced 2025-04-18 23:23:16 +00:00
Simplify and improve stderr parsing
- Allow multiple errors and warnings - Remove ErrorWithPath, which is now replaced by Error - Simplify parsing, allow multiple errors - Add more tests for stderr parsing
This commit is contained in:
parent
188f5c36cb
commit
648c449890
119
errors.go
119
errors.go
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2024 David Vogel
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
//
|
//
|
||||||
// This software is released under the MIT License.
|
// This software is released under the MIT License.
|
||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
@ -8,14 +8,26 @@ package typst
|
|||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents a generic typst error.
|
// ErrorDetails contains the details of a typst.Error.
|
||||||
|
type ErrorDetails struct {
|
||||||
|
Message string // The parsed error message.
|
||||||
|
Path string // Path of the typst file where the error is located in. Zero value means that there is no further information.
|
||||||
|
Line int // Line number of the error. Zero value means that there is no further information.
|
||||||
|
Column int // Column of the error. Zero value means that there is no further information.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error represents a typst error.
|
||||||
|
// This can contain multiple sub-errors or sub-warnings.
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Inner error
|
Inner error
|
||||||
|
|
||||||
Raw string // The raw output from stderr.
|
Raw string // The raw output from stderr.
|
||||||
Message string // The parsed error message.
|
|
||||||
|
// Raw output parsed into errors and warnings.
|
||||||
|
Details []ErrorDetails
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
@ -26,72 +38,43 @@ func (e *Error) Unwrap() error {
|
|||||||
return e.Inner
|
return e.Inner
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorWithPath represents a typst error that also contains information about its origin (filepath, line and column).
|
var stderrRegex = regexp.MustCompile(`(?s)^(?<error>.+?)(?:(?:\n\s+┌─ (?<path>.+?):(?<line>\d+):(?<column>\d+)\n)|(?:$))`)
|
||||||
type ErrorWithPath struct {
|
|
||||||
Inner error
|
|
||||||
|
|
||||||
Raw string // The raw error string as returned by the executable.
|
// ParseStderr will parse the given stderr output and return a typst.Error.
|
||||||
Message string // Error message from typst.
|
|
||||||
|
|
||||||
Path string // Path of the typst file where the error is located in.
|
|
||||||
Line int // Line number of the error.
|
|
||||||
Column int // Column of the error.
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ErrorWithPath) Error() string {
|
|
||||||
return e.Raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ErrorWithPath) Unwrap() error {
|
|
||||||
return e.Inner
|
|
||||||
}
|
|
||||||
|
|
||||||
var stderrRegex = regexp.MustCompile(`(?s)^error: (?<error>.+?)\n`)
|
|
||||||
var stderrWithPathRegex = regexp.MustCompile(`(?s)^error: (?<error>.+?)\n\s+┌─ (?<path>.+?):(?<line>\d+):(?<column>\d+)\n`)
|
|
||||||
|
|
||||||
// ParseStderr will parse the given stderr output and return a suitable error object.
|
|
||||||
// Depending on the stderr message, this will return either a typst.Error or a typst.ErrorWithPath error.
|
|
||||||
func ParseStderr(stderr string, inner error) error {
|
func ParseStderr(stderr string, inner error) error {
|
||||||
if parsed := stderrWithPathRegex.FindStringSubmatch(stderr); parsed != nil {
|
err := Error{
|
||||||
err := ErrorWithPath{
|
|
||||||
Raw: stderr,
|
|
||||||
Inner: inner,
|
|
||||||
}
|
|
||||||
|
|
||||||
if i := stderrWithPathRegex.SubexpIndex("error"); i > 0 && i < len(parsed) {
|
|
||||||
err.Message = parsed[i]
|
|
||||||
}
|
|
||||||
if i := stderrWithPathRegex.SubexpIndex("path"); i > 0 && i < len(parsed) {
|
|
||||||
err.Path = parsed[i]
|
|
||||||
}
|
|
||||||
if i := stderrWithPathRegex.SubexpIndex("line"); i > 0 && i < len(parsed) {
|
|
||||||
line, _ := strconv.ParseInt(parsed[i], 10, 0)
|
|
||||||
err.Line = int(line)
|
|
||||||
}
|
|
||||||
if i := stderrWithPathRegex.SubexpIndex("column"); i > 0 && i < len(parsed) {
|
|
||||||
column, _ := strconv.ParseInt(parsed[i], 10, 0)
|
|
||||||
err.Column = int(column)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &err
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsed := stderrRegex.FindStringSubmatch(stderr); parsed != nil {
|
|
||||||
err := Error{
|
|
||||||
Raw: stderr,
|
|
||||||
Inner: inner,
|
|
||||||
}
|
|
||||||
|
|
||||||
if i := stderrRegex.SubexpIndex("error"); i > 0 && i < len(parsed) {
|
|
||||||
err.Message = parsed[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return &err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to the raw error message.
|
|
||||||
return &Error{
|
|
||||||
Raw: stderr,
|
|
||||||
Inner: inner,
|
Inner: inner,
|
||||||
|
Raw: stderr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all "blocks" ending with double new lines.
|
||||||
|
parts := strings.Split(stderr, "\n\n")
|
||||||
|
parts = parts[:len(parts)-1]
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
if parsed := stderrRegex.FindStringSubmatch(part); parsed != nil {
|
||||||
|
var details ErrorDetails
|
||||||
|
|
||||||
|
if i := stderrRegex.SubexpIndex("error"); i > 0 && i < len(parsed) && parsed[i] != "" {
|
||||||
|
details.Message = parsed[i]
|
||||||
|
}
|
||||||
|
if i := stderrRegex.SubexpIndex("path"); i > 0 && i < len(parsed) && parsed[i] != "" {
|
||||||
|
details.Path = parsed[i]
|
||||||
|
}
|
||||||
|
if i := stderrRegex.SubexpIndex("line"); i > 0 && i < len(parsed) && parsed[i] != "" {
|
||||||
|
if line, err := strconv.ParseInt(parsed[i], 10, 0); err == nil {
|
||||||
|
details.Line = int(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i := stderrRegex.SubexpIndex("column"); i > 0 && i < len(parsed) && parsed[i] != "" {
|
||||||
|
if column, err := strconv.ParseInt(parsed[i], 10, 0); err == nil {
|
||||||
|
details.Column = int(column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err.Details = append(err.Details, details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &err
|
||||||
}
|
}
|
||||||
|
130
errors_test.go
130
errors_test.go
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2024 David Vogel
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
//
|
//
|
||||||
// This software is released under the MIT License.
|
// This software is released under the MIT License.
|
||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Dadido3/go-typst"
|
"github.com/Dadido3/go-typst"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestErrors0(t *testing.T) {
|
func TestErrors0(t *testing.T) {
|
||||||
@ -35,22 +36,26 @@ func TestErrors1(t *testing.T) {
|
|||||||
if err := cli.Compile(r, &w, nil); err == nil {
|
if err := cli.Compile(r, &w, nil); err == nil {
|
||||||
t.Fatalf("Expected error, but got nil")
|
t.Fatalf("Expected error, but got nil")
|
||||||
} else {
|
} else {
|
||||||
var errWithPath *typst.ErrorWithPath
|
var errTypst *typst.Error
|
||||||
if errors.As(err, &errWithPath) {
|
if errors.As(err, &errTypst) {
|
||||||
if errWithPath.Message != "assertion failed: Test" {
|
if len(errTypst.Details) != 1 {
|
||||||
t.Errorf("Expected error with error message %q, got %q", "assertion failed: Test", errWithPath.Message)
|
t.Fatalf("Expected error doesn't contain the expected number of detail entries. Got %v, want %v", len(errTypst.Details), 1)
|
||||||
}
|
}
|
||||||
/*if errWithPath.Path != "" {
|
details := errTypst.Details[0]
|
||||||
t.Errorf("Expected error to point to path %q, got path %q", "", errWithPath.Path)
|
if details.Message != "error: assertion failed: Test" {
|
||||||
|
t.Errorf("Expected error with error message %q, got %q", "error: assertion failed: Test", details.Message)
|
||||||
|
}
|
||||||
|
/*if details.Path != "" {
|
||||||
|
t.Errorf("Expected error to point to path %q, got path %q", "", details.Path)
|
||||||
}*/
|
}*/
|
||||||
if errWithPath.Line != 3 {
|
if details.Line != 3 {
|
||||||
t.Errorf("Expected error to point at line %d, got line %d", 3, errWithPath.Line)
|
t.Errorf("Expected error to point at line %d, got line %d", 3, details.Line)
|
||||||
}
|
}
|
||||||
if errWithPath.Column != 1 {
|
if details.Column != 1 {
|
||||||
t.Errorf("Expected error to point at column %d, got column %d", 1, errWithPath.Column)
|
t.Errorf("Expected error to point at column %d, got column %d", 1, details.Column)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("Expected error type %T, got %T: %v", errWithPath, err, err)
|
t.Errorf("Expected error type %T, got %T: %v", errTypst, err, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,13 +75,108 @@ func TestErrors2(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
var errTypst *typst.Error
|
var errTypst *typst.Error
|
||||||
if errors.As(err, &errTypst) {
|
if errors.As(err, &errTypst) {
|
||||||
|
if len(errTypst.Details) != 1 {
|
||||||
|
t.Fatalf("Expected error doesn't contain the expected number of detail entries. Got %v, want %v", len(errTypst.Details), 1)
|
||||||
|
}
|
||||||
|
details := errTypst.Details[0]
|
||||||
// Don't check the specific error message, as that may change over time.
|
// Don't check the specific error message, as that may change over time.
|
||||||
// The expected message should be similar to: invalid value 'a' for '--pages <PAGES>': not a valid page number.
|
// The expected message should be similar to: error: invalid value 'a' for '--pages <PAGES>': not a valid page number.
|
||||||
if errTypst.Message == "" {
|
if details.Message == "" {
|
||||||
t.Errorf("Expected error message, got %q", errTypst.Message)
|
t.Errorf("Expected error message, got %q", details.Message)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("Expected error type %T, got %T: %v", errTypst, err, err)
|
t.Errorf("Expected error type %T, got %T: %v", errTypst, err, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestErrorParsing(t *testing.T) {
|
||||||
|
var tests = map[string]struct {
|
||||||
|
StdErr string // The original and raw stderr message.
|
||||||
|
ExpectedDetails []typst.ErrorDetails // Expected parsed result.
|
||||||
|
}{
|
||||||
|
"Typst 0.13.0 HTML warning + error": {
|
||||||
|
StdErr: "warning: html export is under active development and incomplete\n = hint: its behaviour may change at any time\n = hint: do not rely on this feature for production use cases\n = hint: see https://github.com/typst/typst/issues/5512 for more information\n\nerror: page configuration is not allowed inside of containers\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:1:1\n │\n1 │ #set page(width: 100mm, height: auto, margin: 5mm)\n │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n",
|
||||||
|
ExpectedDetails: []typst.ErrorDetails{
|
||||||
|
{
|
||||||
|
Message: "warning: html export is under active development and incomplete\n = hint: its behaviour may change at any time\n = hint: do not rely on this feature for production use cases\n = hint: see https://github.com/typst/typst/issues/5512 for more information",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message: "error: page configuration is not allowed inside of containers",
|
||||||
|
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
|
||||||
|
Line: 1,
|
||||||
|
Column: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Typst 0.13.0 error with path": {
|
||||||
|
StdErr: "error: expected expression\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:12:34\n │\n12 │ - Test coverage of most features.#\n │ ^\n\n",
|
||||||
|
ExpectedDetails: []typst.ErrorDetails{
|
||||||
|
{
|
||||||
|
Message: "error: expected expression",
|
||||||
|
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
|
||||||
|
Line: 12,
|
||||||
|
Column: 34,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Typst 0.13.0 multiple errors with paths": {
|
||||||
|
StdErr: "error: expected expression\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:11:53\n │\n11 │ - Uses stdio; No temporary files need to be created.#\n │ ^\n\nerror: expected expression\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:12:34\n │\n12 │ - Test coverage of most features.#\n │ ^\n\n",
|
||||||
|
ExpectedDetails: []typst.ErrorDetails{
|
||||||
|
{
|
||||||
|
Message: "error: expected expression",
|
||||||
|
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
|
||||||
|
Line: 11,
|
||||||
|
Column: 53,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message: "error: expected expression",
|
||||||
|
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
|
||||||
|
Line: 12,
|
||||||
|
Column: 34,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Typst 0.13.0 stacked errors with paths": {
|
||||||
|
StdErr: "error: expected expression\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\test.typ:1:4\n │\n1 │ hey#\n │ ^\n\nhelp: error occurred while importing this module\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:14:9\n │\n14 │ #include \"test.typ\"\n │ ^^^^^^^^^^\n\n",
|
||||||
|
ExpectedDetails: []typst.ErrorDetails{
|
||||||
|
{
|
||||||
|
Message: "error: expected expression",
|
||||||
|
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\test.typ",
|
||||||
|
Line: 1,
|
||||||
|
Column: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message: "help: error occurred while importing this module",
|
||||||
|
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
|
||||||
|
Line: 14,
|
||||||
|
Column: 9,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Typst 0.13.0 error without path": {
|
||||||
|
StdErr: "error: invalid value 'a' for '--pages <PAGES>': not a valid page number\n\nFor more information, try '--help'.\n",
|
||||||
|
ExpectedDetails: []typst.ErrorDetails{
|
||||||
|
{
|
||||||
|
Message: "error: invalid value 'a' for '--pages <PAGES>': not a valid page number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tt := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
result := typst.ParseStderr(tt.StdErr, nil)
|
||||||
|
|
||||||
|
var typstError *typst.Error
|
||||||
|
if errors.As(result, &typstError) {
|
||||||
|
if !cmp.Equal(typstError.Details, tt.ExpectedDetails) {
|
||||||
|
t.Errorf("Parsed details don't match expected details: %s", cmp.Diff(tt.ExpectedDetails, typstError.Details))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Parsed error is not of type %T", typstError)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user