From 034c00b5a0af7d54f9df59c4c932115cffbf3bb3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 6 Jun 2026 14:28:01 +0200 Subject: [PATCH] Implemented deterministic order for error messages --- src/cli/exit.go | 35 ++++++--------------------- src/cli/showError.go | 20 +++++++++++++++ src/cli/showFileError.go | 33 +++++++++++++++++++++++++ src/compiler/Compile.go | 8 +++--- src/compiler/errors.go | 9 +++++++ src/compiler/findError.go | 51 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 31 deletions(-) create mode 100644 src/cli/showError.go create mode 100644 src/cli/showFileError.go create mode 100644 src/compiler/findError.go diff --git a/src/cli/exit.go b/src/cli/exit.go index 3f0ea0f..2cee1d9 100644 --- a/src/cli/exit.go +++ b/src/cli/exit.go @@ -2,14 +2,9 @@ package cli import ( "errors" - "fmt" - "os" "os/exec" - "strings" - fe "git.urbach.dev/cli/q/src/errors" - "git.urbach.dev/go/color" - "git.urbach.dev/go/color/ansi" + "git.urbach.dev/cli/q/src/compiler" ) // Exit codes. @@ -30,33 +25,19 @@ func exit(err error) int { expectedParameter *ExpectedParameter unknownParameter *UnknownParameter invalidValue *InvalidValue - fileError *fe.FileError + multiError *compiler.MultiError ) - if errors.As(err, &fileError) { - line, offset := fileError.Line() - indent := strings.Repeat(" ", offset) - color.Redirect(os.Stderr) - ansi.Reset.Printf("%s\n\n", fileError.Link()) - source := fileError.Source() - length := int(source.End() - source.Start()) - - if length > 0 { - ansi.Reset.Printf(" %s", line[:offset]) - ansi.Red.Print(line[offset : offset+length]) - ansi.Reset.Println(line[offset+length:]) - } else { - ansi.Reset.Printf(" %s\n", line) + if errors.As(err, &multiError) { + for _, err := range multiError.Errors { + showError(err) } - ansi.Red.Printf("%s ┬\n", indent) - ansi.Red.Printf("%s ╰─ ", indent) - ansi.Reset.Printf("%s\n\n", fileError.Error()) - ansi.Dim.Println(fileError.Stack()) - } else { - fmt.Fprintln(os.Stderr, err) + return fail } + showError(err) + if errors.As(err, &exit) { return exit.ExitCode() } diff --git a/src/cli/showError.go b/src/cli/showError.go new file mode 100644 index 0000000..ab07d34 --- /dev/null +++ b/src/cli/showError.go @@ -0,0 +1,20 @@ +package cli + +import ( + "errors" + "fmt" + "os" + + fe "git.urbach.dev/cli/q/src/errors" +) + +// showError shows an error on stderr. +func showError(err error) { + var fileError *fe.FileError + + if errors.As(err, &fileError) { + showFileError(fileError) + } else { + fmt.Fprintln(os.Stderr, err) + } +} \ No newline at end of file diff --git a/src/cli/showFileError.go b/src/cli/showFileError.go new file mode 100644 index 0000000..241d35e --- /dev/null +++ b/src/cli/showFileError.go @@ -0,0 +1,33 @@ +package cli + +import ( + "os" + "strings" + + "git.urbach.dev/cli/q/src/errors" + "git.urbach.dev/go/color" + "git.urbach.dev/go/color/ansi" +) + +// showFileError shows an error on stderr with file, line and column information. +func showFileError(fileError *errors.FileError) { + line, offset := fileError.Line() + indent := strings.Repeat(" ", offset) + color.Redirect(os.Stderr) + ansi.Reset.Printf("%s\n\n", fileError.Link()) + source := fileError.Source() + length := int(source.End() - source.Start()) + + if length > 0 { + ansi.Reset.Printf(" %s", line[:offset]) + ansi.Red.Print(line[offset : offset+length]) + ansi.Reset.Println(line[offset+length:]) + } else { + ansi.Reset.Printf(" %s\n", line) + } + + ansi.Red.Printf("%s ┬\n", indent) + ansi.Red.Printf("%s ╰─ ", indent) + ansi.Reset.Printf("%s\n\n", fileError.Error()) + ansi.Dim.Println(fileError.Stack()) +} \ No newline at end of file diff --git a/src/compiler/Compile.go b/src/compiler/Compile.go index 774788c..bf089cc 100644 --- a/src/compiler/Compile.go +++ b/src/compiler/Compile.go @@ -49,10 +49,10 @@ func Compile(build *config.Build) (*core.Environment, error) { }) // Report errors if any occurred - for f := range env.Functions() { - if f.Err != nil { - return nil, f.Err - } + err = findError(env.Functions()) + + if err != nil { + return nil, err } // Check for unused imports in all files diff --git a/src/compiler/errors.go b/src/compiler/errors.go index 9643789..40b88ef 100644 --- a/src/compiler/errors.go +++ b/src/compiler/errors.go @@ -11,6 +11,15 @@ var ( MissingMainFunction = errors.String("Missing main function") ) +// MultiError error is created when there is more than one error. +type MultiError struct { + Errors []error +} + +func (err *MultiError) Error() string { + return "" +} + // UnusedImport error is created when an import is never used. type UnusedImport struct { Package string diff --git a/src/compiler/findError.go b/src/compiler/findError.go new file mode 100644 index 0000000..05569e1 --- /dev/null +++ b/src/compiler/findError.go @@ -0,0 +1,51 @@ +package compiler + +import ( + "iter" + "slices" + "strings" + + "git.urbach.dev/cli/q/src/core" + "git.urbach.dev/cli/q/src/errors" +) + +// findError returns compilation errors and creates a +// deterministic order in case of multiple errors. +func findError(functions iter.Seq[*core.Function]) error { + var errs []error + + for f := range functions { + if f.Err != nil { + errs = append(errs, f.Err) + } + } + + if len(errs) == 0 { + return nil + } + + if len(errs) == 1 { + return errs[0] + } + + slices.SortFunc(errs, func(errA error, errB error) int { + a := errA.(*errors.FileError) + b := errB.(*errors.FileError) + diff := strings.Compare(a.File().Path, b.File().Path) + + if diff != 0 { + return diff + } + + lineA, columnA := a.LineColumn() + lineB, columnB := b.LineColumn() + + if lineA == lineB { + return columnA - columnB + } + + return lineA - lineB + }) + + return &MultiError{Errors: errs} +} \ No newline at end of file -- 2.51.2