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