diff --git a/src/errors/FileError.go b/src/errors/FileError.go new file mode 100644 index 0000000..d58bf71 --- /dev/null +++ b/src/errors/FileError.go @@ -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 +} \ No newline at end of file diff --git a/src/errors/FileError_test.go b/src/errors/FileError_test.go new file mode 100644 index 0000000..49ac71e --- /dev/null +++ b/src/errors/FileError_test.go @@ -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 +} \ No newline at end of file diff --git a/src/errors/New.go b/src/errors/New.go new file mode 100644 index 0000000..b4c9fc4 --- /dev/null +++ b/src/errors/New.go @@ -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(), + } +} \ No newline at end of file diff --git a/src/errors/stack.go b/src/errors/stack.go new file mode 100644 index 0000000..0d98b7e --- /dev/null +++ b/src/errors/stack.go @@ -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") +} \ No newline at end of file