Added errors package
All checks were successful
/ test (push) Successful in 13s

This commit is contained in:
Eduard Urbach 2025-06-19 16:56:30 +02:00
parent 1405d2b8b1
commit b7fdd56a8b
Signed by: akyoto
GPG key ID: 49226B848C78F6C8
6 changed files with 172 additions and 0 deletions

68
src/errors/FileError.go Normal file
View 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
}

View 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
View 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
View 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")
}

View file

@ -1,8 +1,11 @@
package fs package fs
import "git.urbach.dev/cli/q/src/token"
// File represents a single source file. // File represents a single source file.
type File struct { type File struct {
Path string Path string
Package string Package string
Bytes []byte Bytes []byte
Tokens token.List
} }

View file

@ -8,6 +8,7 @@ import (
"git.urbach.dev/cli/q/src/build" "git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/fs" "git.urbach.dev/cli/q/src/fs"
"git.urbach.dev/cli/q/src/token"
) )
// scanner is used to scan files before the actual compilation step. // scanner is used to scan files before the actual compilation step.
@ -130,10 +131,13 @@ func (s *scanner) scanFile(path string, pkg string) error {
return err return err
} }
tokens := token.Tokenize(contents)
file := &fs.File{ file := &fs.File{
Path: path, Path: path,
Package: pkg, Package: pkg,
Bytes: contents, Bytes: contents,
Tokens: tokens,
} }
s.files <- file s.files <- file