This commit is contained in:
parent
1405d2b8b1
commit
ee1b7f4e44
4 changed files with 165 additions and 0 deletions
68
src/errors/FileError.go
Normal file
68
src/errors/FileError.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.urbach.dev/cli/q/src/fs"
|
||||||
|
"git.urbach.dev/cli/q/src/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileError is an error inside a file at a given line and column.
|
||||||
|
type FileError struct {
|
||||||
|
err error
|
||||||
|
file *fs.File
|
||||||
|
position token.Position
|
||||||
|
stack string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface.
|
||||||
|
func (e *FileError) Error() string {
|
||||||
|
path := e.Path()
|
||||||
|
line, column := e.LineColumn()
|
||||||
|
return fmt.Sprintf("%s:%d:%d: %s\n\n%s", path, line, column, e.err, e.stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineColumn returns the line and column of the error.
|
||||||
|
func (e *FileError) LineColumn() (int, int) {
|
||||||
|
line := 1
|
||||||
|
column := 1
|
||||||
|
lineStart := -1
|
||||||
|
|
||||||
|
for _, t := range e.file.Tokens {
|
||||||
|
if t.Position >= e.position {
|
||||||
|
column = int(e.position) - lineStart
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind == token.NewLine {
|
||||||
|
lineStart = int(t.Position)
|
||||||
|
line++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return line, column
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the relative path of the file to shorten the error message.
|
||||||
|
func (e *FileError) Path() string {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return e.file.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
relative, err := filepath.Rel(cwd, e.file.Path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return e.file.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
return relative
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the wrapped error.
|
||||||
|
func (e *FileError) Unwrap() error {
|
||||||
|
return e.err
|
||||||
|
}
|
49
src/errors/FileError_test.go
Normal file
49
src/errors/FileError_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package errors_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.urbach.dev/cli/q/src/errors"
|
||||||
|
"git.urbach.dev/cli/q/src/fs"
|
||||||
|
"git.urbach.dev/cli/q/src/token"
|
||||||
|
"git.urbach.dev/go/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAbsolutePath(t *testing.T) {
|
||||||
|
relPath := "../../examples/hello/hello.q"
|
||||||
|
absPath, abserr := filepath.Abs(relPath)
|
||||||
|
assert.Nil(t, abserr)
|
||||||
|
err := test(t, absPath)
|
||||||
|
assert.Equal(t, err.Path(), relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRelativePath(t *testing.T) {
|
||||||
|
relPath := "../../examples/hello/hello.q"
|
||||||
|
err := test(t, relPath)
|
||||||
|
assert.Equal(t, err.Path(), relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func test(t *testing.T, path string) *errors.FileError {
|
||||||
|
contents, oserr := os.ReadFile(path)
|
||||||
|
assert.Nil(t, oserr)
|
||||||
|
tokens := token.Tokenize(contents)
|
||||||
|
|
||||||
|
file := &fs.File{
|
||||||
|
Path: path,
|
||||||
|
Bytes: contents,
|
||||||
|
Tokens: tokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := errors.New(io.EOF, file, 11)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
line, column := err.LineColumn()
|
||||||
|
assert.Equal(t, line, 3)
|
||||||
|
assert.Equal(t, column, 1)
|
||||||
|
assert.Equal(t, err.Unwrap().Error(), "EOF")
|
||||||
|
assert.Contains(t, err.Error(), ":3:1: EOF")
|
||||||
|
return err
|
||||||
|
}
|
18
src/errors/New.go
Normal file
18
src/errors/New.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.urbach.dev/cli/q/src/fs"
|
||||||
|
"git.urbach.dev/cli/q/src/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New generates an error message at the current token position.
|
||||||
|
// The error message is clickable in popular editors and leads you
|
||||||
|
// directly to the faulty file at the given line and position.
|
||||||
|
func New(err error, file *fs.File, position token.Position) *FileError {
|
||||||
|
return &FileError{
|
||||||
|
err: err,
|
||||||
|
file: file,
|
||||||
|
position: position,
|
||||||
|
stack: stack(),
|
||||||
|
}
|
||||||
|
}
|
30
src/errors/stack.go
Normal file
30
src/errors/stack.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stack generates a stack trace.
|
||||||
|
func stack() string {
|
||||||
|
var (
|
||||||
|
final []string
|
||||||
|
buffer = make([]byte, 4096)
|
||||||
|
n = runtime.Stack(buffer, false)
|
||||||
|
stack = string(buffer[:n])
|
||||||
|
lines = strings.Split(stack, "\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 6; i < len(lines); i += 2 {
|
||||||
|
line := strings.TrimSpace(lines[i])
|
||||||
|
space := strings.LastIndex(line, " ")
|
||||||
|
|
||||||
|
if space != -1 {
|
||||||
|
line = line[:space]
|
||||||
|
}
|
||||||
|
|
||||||
|
final = append(final, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(final, "\n")
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue