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