Initial support for printing out diagnostics
The indication of specific characters in the source code that are in error is not yet implemented, but this gets at the main functionality of printing diagnostics.
This commit is contained in:
parent
cdefa254c9
commit
ca6c6b127c
@ -95,3 +95,9 @@ func (d Diagnostics) HasErrors() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A DiagnosticWriter emits diagnostics somehow.
|
||||
type DiagnosticWriter interface {
|
||||
WriteDiagnostic(*Diagnostic) error
|
||||
WriteDiagnostics(Diagnostics) error
|
||||
}
|
||||
|
152
zcl/diagnostic_text.go
Normal file
152
zcl/diagnostic_text.go
Normal file
@ -0,0 +1,152 @@
|
||||
package zcl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
wordwrap "github.com/mitchellh/go-wordwrap"
|
||||
)
|
||||
|
||||
type diagnosticTextWriter struct {
|
||||
sources map[string][]byte
|
||||
wr io.Writer
|
||||
width uint
|
||||
color bool
|
||||
}
|
||||
|
||||
// NewDiagnosticTextWriter creates a DiagnosticWriter that writes diagnostics
|
||||
// to the given writer as formatted text.
|
||||
//
|
||||
// It is designed to produce text appropriate to print in a monospaced font
|
||||
// in a terminal of a particular width, or optionally with no width limit.
|
||||
//
|
||||
// The given width may be zero to disable word-wrapping of the detail text
|
||||
// and truncation of source code snippets.
|
||||
//
|
||||
// If color is set to true, the output will include VT100 escape sequences to
|
||||
// color-code the severity indicators. It is suggested to turn this off if
|
||||
// the target writer is not a terminal.
|
||||
func NewDiagnosticTextWriter(wr io.Writer, sources map[string][]byte, width uint, color bool) DiagnosticWriter {
|
||||
return &diagnosticTextWriter{
|
||||
sources: sources,
|
||||
wr: wr,
|
||||
width: width,
|
||||
color: color,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error {
|
||||
if diag == nil {
|
||||
return errors.New("nil diagnostic")
|
||||
}
|
||||
|
||||
var colorCode, resetCode string
|
||||
if w.color {
|
||||
switch diag.Severity {
|
||||
case DiagError:
|
||||
colorCode = "\x1b[31m"
|
||||
case DiagWarning:
|
||||
colorCode = "\x1b[33m"
|
||||
}
|
||||
resetCode = "\x1b[0m"
|
||||
}
|
||||
|
||||
var severityStr string
|
||||
switch diag.Severity {
|
||||
case DiagError:
|
||||
severityStr = "Error"
|
||||
case DiagWarning:
|
||||
severityStr = "Warning"
|
||||
default:
|
||||
// should never happen
|
||||
severityStr = "???????"
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.wr, "%s%s%s: %s\n\n", colorCode, severityStr, resetCode, diag.Summary)
|
||||
|
||||
if diag.Subject != nil {
|
||||
|
||||
src := w.sources[diag.Subject.Filename]
|
||||
if src == nil {
|
||||
fmt.Fprintf(w.wr, " on %s line %d:\n (source code not available)\n\n", diag.Subject.Filename, diag.Subject.Start.Line)
|
||||
} else {
|
||||
r := bytes.NewReader(src)
|
||||
sc := bufio.NewScanner(r)
|
||||
sc.Split(bufio.ScanLines)
|
||||
contextLine := ""
|
||||
|
||||
var startLine, endLine int
|
||||
if diag.Context != nil {
|
||||
startLine = diag.Context.Start.Line
|
||||
endLine = diag.Context.End.Line
|
||||
} else {
|
||||
startLine = diag.Subject.Start.Line
|
||||
endLine = diag.Subject.End.Line
|
||||
}
|
||||
|
||||
li := 1
|
||||
var ls string
|
||||
for sc.Scan() {
|
||||
ls = sc.Text()
|
||||
if len(ls) > 0 && strings.Contains(ls, "{") && ls[0] != ' ' && ls[0] != '\t' && ls[0] != '#' && ls[0] != '/' && ls[0] != '{' {
|
||||
// Keep track of the latest non-space-prefixed line we've
|
||||
// seen, to use as context. This is a pretty sloppy way
|
||||
// to do this, but it works well enough for most normal
|
||||
// files. (In particular though, it doesn't work for JSON sources.)
|
||||
contextLine = fmt.Sprintf(", in %s", strings.Replace(ls, " {", "", 1))
|
||||
}
|
||||
|
||||
if li == startLine {
|
||||
break
|
||||
}
|
||||
li++
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.wr, " on %s line %d%s:\n", diag.Subject.Filename, diag.Subject.Start.Line, contextLine)
|
||||
|
||||
// TODO: Generate markers for the specific characters that are in the Context and Subject ranges.
|
||||
// For now, we just print out the lines.
|
||||
|
||||
fmt.Fprintf(w.wr, "%4d: %s\n", li, ls)
|
||||
|
||||
if endLine > li {
|
||||
for sc.Scan() {
|
||||
ls = sc.Text()
|
||||
li++
|
||||
|
||||
fmt.Fprintf(w.wr, "%4d: %s\n", li, ls)
|
||||
|
||||
if li == endLine {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.wr.Write([]byte{'\n'})
|
||||
}
|
||||
}
|
||||
|
||||
if diag.Detail != "" {
|
||||
detail := diag.Detail
|
||||
if w.width != 0 {
|
||||
detail = wordwrap.WrapString(detail, w.width)
|
||||
}
|
||||
fmt.Fprintf(w.wr, "%s\n\n", detail)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error {
|
||||
for _, diag := range diags {
|
||||
err := w.WriteDiagnostic(diag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
135
zcl/diagnostic_text_test.go
Normal file
135
zcl/diagnostic_text_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
package zcl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDiagnosticTextWriter(t *testing.T) {
|
||||
tests := []struct {
|
||||
Input *Diagnostic
|
||||
Want string
|
||||
}{
|
||||
{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Splines not reticulated",
|
||||
Detail: "All splines must be pre-reticulated.",
|
||||
Subject: &Range{
|
||||
Start: Pos{
|
||||
Byte: 0,
|
||||
Column: 1,
|
||||
Line: 1,
|
||||
},
|
||||
End: Pos{
|
||||
Byte: 3,
|
||||
Column: 4,
|
||||
Line: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
`Error: Splines not reticulated
|
||||
|
||||
on line 1:
|
||||
1: foo = 1
|
||||
|
||||
All splines must be pre-reticulated.
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Unsupported attribute",
|
||||
Detail: `"baz" is not a supported top-level attribute. Did you mean "bam"?`,
|
||||
Subject: &Range{
|
||||
Start: Pos{
|
||||
Column: 1,
|
||||
Line: 3,
|
||||
},
|
||||
End: Pos{
|
||||
Column: 4,
|
||||
Line: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
`Error: Unsupported attribute
|
||||
|
||||
on line 3:
|
||||
3: baz = 3
|
||||
|
||||
"baz" is not a supported top-level
|
||||
attribute. Did you mean "bam"?
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Unsupported attribute",
|
||||
Detail: `"pizza" is not a supported attribute. Did you mean "pizzetta"?`,
|
||||
Subject: &Range{
|
||||
Start: Pos{
|
||||
Column: 3,
|
||||
Line: 5,
|
||||
},
|
||||
End: Pos{
|
||||
Column: 8,
|
||||
Line: 5,
|
||||
},
|
||||
},
|
||||
// This is actually not a great example of a context, but is here to test
|
||||
// whether we're able to show a multi-line context when needed.
|
||||
Context: &Range{
|
||||
Start: Pos{
|
||||
Column: 1,
|
||||
Line: 4,
|
||||
},
|
||||
End: Pos{
|
||||
Column: 2,
|
||||
Line: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
`Error: Unsupported attribute
|
||||
|
||||
on line 5, in block "party":
|
||||
4: block "party" {
|
||||
5: pizza = "cheese"
|
||||
6: }
|
||||
|
||||
"pizza" is not a supported attribute.
|
||||
Did you mean "pizzetta"?
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
sources := map[string][]byte{
|
||||
"": []byte(testDiagnosticTextWriterSource),
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
bwr := &bytes.Buffer{}
|
||||
dwr := NewDiagnosticTextWriter(bwr, sources, 40, false)
|
||||
err := dwr.WriteDiagnostic(test.Input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
got := bwr.String()
|
||||
if got != test.Want {
|
||||
t.Errorf("wrong result\n\ngot:\n%swant:\n%s", got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const testDiagnosticTextWriterSource = `foo = 1
|
||||
bar = 2
|
||||
baz = 3
|
||||
block "party" {
|
||||
pizza = "cheese"
|
||||
}
|
||||
`
|
Loading…
Reference in New Issue
Block a user