blob: 5458846857d518d74ee870246bc008798cbf485d [file] [log] [blame]
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package driverutil
// This file defined output helpers common to all drivers.
import (
"cmp"
"encoding/json"
"fmt"
"go/token"
"io"
"log"
"os"
"strings"
"golang.org/x/tools/go/analysis"
)
// TODO(adonovan): don't accept an io.Writer if we don't report errors.
// Either accept a bytes.Buffer (infallible), or return a []byte.
// PrintPlain prints a diagnostic in plain text form.
// If contextLines is nonnegative, it also prints the
// offending line plus this many lines of context.
func PrintPlain(out io.Writer, fset *token.FileSet, contextLines int, diag analysis.Diagnostic) {
print := func(pos, end token.Pos, message string) {
posn := fset.Position(pos)
fmt.Fprintf(out, "%s: %s\n", posn, message)
// show offending line plus N lines of context.
if contextLines >= 0 {
end := fset.Position(end)
if !end.IsValid() {
end = posn
}
// TODO(adonovan): highlight the portion of the line indicated
// by pos...end using ASCII art, terminal colors, etc?
data, _ := os.ReadFile(posn.Filename)
lines := strings.Split(string(data), "\n")
for i := posn.Line - contextLines; i <= end.Line+contextLines; i++ {
if 1 <= i && i <= len(lines) {
fmt.Fprintf(out, "%d\t%s\n", i, lines[i-1])
}
}
}
}
print(diag.Pos, diag.End, diag.Message)
for _, rel := range diag.Related {
print(rel.Pos, rel.End, "\t"+rel.Message)
}
}
// A JSONTree is a mapping from package ID to analysis name to result.
// Each result is either a jsonError or a list of JSONDiagnostic.
type JSONTree map[string]map[string]any
// A TextEdit describes the replacement of a portion of a file.
// Start and End are zero-based half-open indices into the original byte
// sequence of the file, and New is the new text.
type JSONTextEdit struct {
Filename string `json:"filename"`
Start int `json:"start"`
End int `json:"end"`
New string `json:"new"`
}
// A JSONSuggestedFix describes an edit that should be applied as a whole or not
// at all. It might contain multiple TextEdits/text_edits if the SuggestedFix
// consists of multiple non-contiguous edits.
type JSONSuggestedFix struct {
Message string `json:"message"`
Edits []JSONTextEdit `json:"edits"`
}
// A JSONDiagnostic describes the JSON schema of an analysis.Diagnostic.
type JSONDiagnostic struct {
Category string `json:"category,omitempty"`
Posn string `json:"posn"` // e.g. "file.go:line:column"
End string `json:"end"` // (ditto)
Message string `json:"message"`
SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"`
Related []JSONRelatedInformation `json:"related,omitempty"`
}
// A JSONRelated describes a secondary position and message related to
// a primary diagnostic.
type JSONRelatedInformation struct {
Posn string `json:"posn"` // e.g. "file.go:line:column"
End string `json:"end"` // (ditto)
Message string `json:"message"`
}
// Add adds the result of analysis 'name' on package 'id'.
// The result is either a list of diagnostics or an error.
func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) {
var v any
if err != nil {
type jsonError struct {
Err string `json:"error"`
}
v = jsonError{err.Error()}
} else if len(diags) > 0 {
diagnostics := make([]JSONDiagnostic, 0, len(diags))
for _, f := range diags {
var fixes []JSONSuggestedFix
for _, fix := range f.SuggestedFixes {
var edits []JSONTextEdit
for _, edit := range fix.TextEdits {
edits = append(edits, JSONTextEdit{
Filename: fset.Position(edit.Pos).Filename,
Start: fset.Position(edit.Pos).Offset,
End: fset.Position(edit.End).Offset,
New: string(edit.NewText),
})
}
fixes = append(fixes, JSONSuggestedFix{
Message: fix.Message,
Edits: edits,
})
}
var related []JSONRelatedInformation
for _, r := range f.Related {
related = append(related, JSONRelatedInformation{
Posn: fset.Position(r.Pos).String(),
End: fset.Position(cmp.Or(r.End, r.Pos)).String(),
Message: r.Message,
})
}
jdiag := JSONDiagnostic{
Category: f.Category,
Posn: fset.Position(f.Pos).String(),
End: fset.Position(cmp.Or(f.End, f.Pos)).String(),
Message: f.Message,
SuggestedFixes: fixes,
Related: related,
}
diagnostics = append(diagnostics, jdiag)
}
v = diagnostics
}
if v != nil {
m, ok := tree[id]
if !ok {
m = make(map[string]any)
tree[id] = m
}
m[name] = v
}
}
func (tree JSONTree) Print(out io.Writer) error {
data, err := json.MarshalIndent(tree, "", "\t")
if err != nil {
log.Panicf("internal error: JSON marshaling failed: %v", err)
}
_, err = fmt.Fprintf(out, "%s\n", data)
return err
}