This change adds colorization to the core Mu tool's output, similar
to what we added to MuJS in
cf6bbd460d
.
193 lines
4.8 KiB
Go
193 lines
4.8 KiB
Go
// Copyright 2016 Marapongo, Inc. All rights reserved.
|
|
|
|
package diag
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"github.com/golang/glog"
|
|
"github.com/reconquest/loreley"
|
|
|
|
"github.com/marapongo/mu/pkg/util/contract"
|
|
)
|
|
|
|
// Sink facilitates pluggable diagnostics messages.
|
|
type Sink interface {
|
|
// Count fetches the total number of diagnostics issued (errors plus warnings).
|
|
Count() int
|
|
// Errors fetches the number of errors issued.
|
|
Errors() int
|
|
// Warnings fetches the number of warnings issued.
|
|
Warnings() int
|
|
// Success returns true if this sink is currently error-free.
|
|
Success() bool
|
|
|
|
// Error issues a new error diagnostic.
|
|
Errorf(diag *Diag, args ...interface{})
|
|
// Warning issues a new warning diagnostic.
|
|
Warningf(diag *Diag, args ...interface{})
|
|
|
|
// Stringify stringifies a diagnostic in the usual way (e.g., "error: MU123: Mu.yaml:7:39: error goes here\n").
|
|
Stringify(diag *Diag, cat Category, args ...interface{}) string
|
|
}
|
|
|
|
// Category dictates the kind of diagnostic.
|
|
type Category string
|
|
|
|
const (
|
|
Error Category = "error"
|
|
Warning = "warning"
|
|
)
|
|
|
|
// FormatOptions controls the output style and content.
|
|
type FormatOptions struct {
|
|
Pwd string // the working directory.
|
|
Colors bool // if true, output will be colorized.
|
|
}
|
|
|
|
// DefaultSink returns a default sink that simply logs output to stderr/stdout.
|
|
func DefaultSink(opts FormatOptions) Sink {
|
|
return newDefaultSink(opts, os.Stderr, os.Stdout)
|
|
}
|
|
|
|
func newDefaultSink(opts FormatOptions, errorW io.Writer, warningW io.Writer) *defaultSink {
|
|
return &defaultSink{opts: opts, errorW: errorW, warningW: warningW}
|
|
}
|
|
|
|
const DefaultSinkIDPrefix = "MU"
|
|
|
|
// defaultSink is the default sink which logs output to stderr/stdout.
|
|
type defaultSink struct {
|
|
opts FormatOptions // a set of options that control output style and content.
|
|
errors int // the number of errors that have been issued.
|
|
errorW io.Writer // the output stream to use for errors.
|
|
warnings int // the number of warnings that have been issued.
|
|
warningW io.Writer // the output stream to use for warnings.
|
|
}
|
|
|
|
func (d *defaultSink) Count() int {
|
|
return d.errors + d.warnings
|
|
}
|
|
|
|
func (d *defaultSink) Errors() int {
|
|
return d.errors
|
|
}
|
|
|
|
func (d *defaultSink) Warnings() int {
|
|
return d.warnings
|
|
}
|
|
|
|
func (d *defaultSink) Success() bool {
|
|
return d.errors == 0
|
|
}
|
|
|
|
func (d *defaultSink) Errorf(diag *Diag, args ...interface{}) {
|
|
msg := d.Stringify(diag, Error, args...)
|
|
if glog.V(3) {
|
|
glog.V(3).Infof("defaultSink::Error(%v)", msg[:len(msg)-1])
|
|
}
|
|
fmt.Fprintf(d.errorW, msg)
|
|
d.errors++
|
|
}
|
|
|
|
func (d *defaultSink) Warningf(diag *Diag, args ...interface{}) {
|
|
msg := d.Stringify(diag, Warning, args...)
|
|
if glog.V(4) {
|
|
glog.V(4).Infof("defaultSink::Warning(%v)", msg[:len(msg)-1])
|
|
}
|
|
fmt.Fprintf(d.warningW, msg)
|
|
d.warnings++
|
|
}
|
|
|
|
func (d *defaultSink) Stringify(diag *Diag, cat Category, args ...interface{}) string {
|
|
var buffer bytes.Buffer
|
|
|
|
// First print the location if there is one.
|
|
if diag.Doc != nil {
|
|
if d.opts.Colors {
|
|
buffer.WriteString("{fg 6}") // cyan
|
|
}
|
|
|
|
file := diag.Doc.File
|
|
if d.opts.Pwd != "" {
|
|
// If a PWD is available, try to create a relative path.
|
|
rel, err := filepath.Rel(d.opts.Pwd, file)
|
|
if err == nil {
|
|
file = rel
|
|
}
|
|
}
|
|
buffer.WriteString(file)
|
|
|
|
if diag.Loc != nil && !diag.Loc.IsEmpty() {
|
|
buffer.WriteRune('(')
|
|
buffer.WriteString(strconv.Itoa(diag.Loc.Start.Line))
|
|
buffer.WriteRune(',')
|
|
buffer.WriteString(strconv.Itoa(diag.Loc.Start.Column))
|
|
buffer.WriteRune(')')
|
|
}
|
|
buffer.WriteString(": ")
|
|
|
|
if d.opts.Colors {
|
|
buffer.WriteString("{reset}")
|
|
}
|
|
}
|
|
|
|
// Now print the message category's prefix (error/warning).
|
|
if d.opts.Colors {
|
|
switch cat {
|
|
case Error:
|
|
buffer.WriteString("{fg 1}") // red
|
|
case Warning:
|
|
buffer.WriteString("{fg 11}") // bright yellow
|
|
default:
|
|
contract.Failf("Unrecognized diagnostic category: %v", cat)
|
|
}
|
|
}
|
|
|
|
buffer.WriteString(string(cat))
|
|
|
|
if diag.ID > 0 {
|
|
buffer.WriteString(" ")
|
|
buffer.WriteString(DefaultSinkIDPrefix)
|
|
buffer.WriteString(strconv.Itoa(int(diag.ID)))
|
|
}
|
|
|
|
buffer.WriteString(": ")
|
|
|
|
if d.opts.Colors {
|
|
buffer.WriteString("{reset}")
|
|
}
|
|
|
|
// Finally, actually print the message itself.
|
|
if d.opts.Colors {
|
|
buffer.WriteString("{fg 7}") // white
|
|
}
|
|
|
|
buffer.WriteString(fmt.Sprintf(diag.Message, args...))
|
|
|
|
if d.opts.Colors {
|
|
buffer.WriteString("{reset}")
|
|
}
|
|
|
|
buffer.WriteRune('\n')
|
|
|
|
// TODO[marapongo/mu#15]: support Clang-style expressive diagnostics. This would entail, for example, using the
|
|
// buffer within the target document, to demonstrate the offending line/column range of code.
|
|
|
|
s := buffer.String()
|
|
|
|
// If colorization was requested, compile and execute the directives now.
|
|
if d.opts.Colors {
|
|
var err error
|
|
s, err = loreley.CompileAndExecuteToString(s, nil, nil)
|
|
contract.Assert(err == nil)
|
|
}
|
|
|
|
return s
|
|
}
|