package shared
import (
"fmt"
"os"
"runtime"
"strings"
)
type Error struct {
message string
loc Location
isWarning bool
warningKind WarningKind
notes []DiagnosticNote
}
type DiagnosticNote struct {
message string
loc Location
}
type WarningKind string
const (
WarningUnusedVariable WarningKind = "unused-variable"
WarningUnusedParameter WarningKind = "unused-parameter"
)
func NewError(loc Location, message string, a ...any) Error {
return Error{
message: fmt.Sprintf(message, a...),
loc: loc,
isWarning: false,
}
}
func NewWarning(kind WarningKind, loc Location, message string, a ...any) Error {
return Error{
message: fmt.Sprintf(message, a...),
loc: loc,
isWarning: true,
warningKind: kind,
}
}
func (err Error) WarningKind() WarningKind { return err.warningKind }
func (err Error) AsError() Error {
err.isWarning = false
return err
}
// WithNote adds a source-located explanation to a primary diagnostic.
func (err Error) WithNote(loc Location, message string, a ...any) Error {
err.notes = append(err.notes, DiagnosticNote{loc: loc, message: fmt.Sprintf(message, a...)})
return err
}
func (err Error) Error() string {
source := err.loc.SourceText
if source == "" {
f, e := os.ReadFile(err.loc.FilePath)
if e == nil {
source = string(f)
}
}
if source == "" {
if err.loc.FilePath != "" && err.loc.LC.Line > 0 {
kind := "Error"
if err.isWarning {
kind = "Warning"
}
return fmt.Sprintf("%s: %s:%d:%d\n\n%s", kind, err.loc.FilePath,
err.loc.LC.Line, err.loc.LC.Col, err.message)
}
return err.message
}
lines := strings.Split(source, "\n")
startLineIdx := err.loc.LC.Line - 1
if startLineIdx < 0 || startLineIdx >= len(lines) {
return err.message
}
endLC := err.loc.EndLC
if endLC.Line <= 0 || endLC.Col <= 0 ||
endLC.Line < err.loc.LC.Line ||
(endLC.Line == err.loc.LC.Line && endLC.Col <= err.loc.LC.Col) {
endLC = LineCol{Line: err.loc.LC.Line, Col: err.loc.LC.Col + 1}
}
endLineIdx := min(len(lines)-1, endLC.Line-1)
start := max(0, startLineIdx-2)
end := min(len(lines)-1, endLineIdx+2)
var b strings.Builder
if err.isWarning {
b.WriteString("Warning: ")
} else {
b.WriteString("Error: ")
}
fmt.Fprintf(&b, "%s:%d:%d\n", err.loc.FilePath, err.loc.LC.Line, err.loc.LC.Col)
red := "\x1b[31m"
yellow := "\x1b[33m"
reset := "\x1b[0m"
if runtime.GOOS == "windows" || os.Getenv("NO_COLOR") != "" {
red = ""
yellow = ""
reset = ""
}
var color string
if err.isWarning {
color = yellow
} else {
color = red
}
for i := start; i <= end; i++ {
ln := i + 1
if i < startLineIdx || i > endLineIdx {
fmt.Fprintf(&b, "%4d | %s\n", ln, lines[i])
continue
}
line := []rune(lines[i])
highlightStart := 0
if i == startLineIdx {
highlightStart = max(0, err.loc.LC.Col-1)
}
highlightEnd := len(line)
if i == endLineIdx {
highlightEnd = max(0, endLC.Col-1)
}
highlightStart = min(highlightStart, len(line))
highlightEnd = min(max(highlightEnd, highlightStart), len(line))
if highlightEnd == highlightStart && highlightStart < len(line) {
highlightEnd++
}
fmt.Fprintf(&b, "%s%4d |%s %s%s%s%s\n", color, ln, reset,
string(line[:highlightStart]), color, string(line[highlightStart:highlightEnd]), reset+string(line[highlightEnd:]))
if color == "" {
markerWidth := max(1, highlightEnd-highlightStart)
fmt.Fprintf(&b, " | %s%s\n", strings.Repeat(" ", highlightStart), strings.Repeat("^", markerWidth))
}
}
fmt.Fprintf(&b, "\n%s", err.message)
for _, note := range err.notes {
formatted := NewWarning("", note.loc, note.message).Error()
formatted = strings.Replace(formatted, "Warning: ", "Note: ", 1)
b.WriteString("\n\n")
b.WriteString(formatted)
}
return b.String()
}
type LineCol struct {
Line int
Col int
}
type Location struct {
LC LineCol
EndLC LineCol
Offset int
EndOffset int
FilePath string
SourceText string
}
func (l Location) WithEnd(end Location) Location {
l.EndLC = end.EndLC
l.EndOffset = end.EndOffset
if l.EndLC.Line == 0 {
l.EndLC = end.LC
l.EndOffset = end.Offset
}
return l
}
func (l Location) String() string {
return fmt.Sprintf("%s:%d:%d", l.FilePath, l.LC.Line, l.LC.Col)
}
type Pair[T1 any, T2 any] struct {
L T1
R T2
}