| // 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 | |
| } |