x/tools/go/analysis: extend json output by SuggestedFixes
+ The JSON output now contains SuggestedFixes from the Diagnostics. The edits
are encoded as replacements for ranges defined by 0 based byte start and end
indicies into the original file.
+ This change also exports the structs that are used for JSON encoding
and thus documents the JSON schema.
Fixes golang/go#55138
Change-Id: I93411626279a866de2986ff78d93775a86ae2213
Reviewed-on: https://go-review.googlesource.com/c/tools/+/435255
Reviewed-by: Tim King <taking@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Run-TryBot: Cherry Mui <cherryyz@google.com>
diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go
index 4b7be2d..2ea6306 100644
--- a/go/analysis/internal/analysisflags/flags.go
+++ b/go/analysis/internal/analysisflags/flags.go
@@ -339,9 +339,38 @@
}
// A JSONTree is a mapping from package ID to analysis name to result.
-// Each result is either a jsonError or a list of jsonDiagnostic.
+// Each result is either a jsonError or a list of JSONDiagnostic.
type JSONTree map[string]map[string]interface{}
+// 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 can be used to encode and decode analysis.Diagnostics to and
+// from JSON.
+// TODO(matloob): Should the JSON diagnostics contain ranges?
+// If so, how should they be formatted?
+type JSONDiagnostic struct {
+ Category string `json:"category,omitempty"`
+ Posn string `json:"posn"`
+ Message string `json:"message"`
+ SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"`
+}
+
// 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) {
@@ -352,20 +381,31 @@
}
v = jsonError{err.Error()}
} else if len(diags) > 0 {
- type jsonDiagnostic struct {
- Category string `json:"category,omitempty"`
- Posn string `json:"posn"`
- Message string `json:"message"`
- }
- var diagnostics []jsonDiagnostic
- // TODO(matloob): Should the JSON diagnostics contain ranges?
- // If so, how should they be formatted?
+ diagnostics := make([]JSONDiagnostic, 0, len(diags))
for _, f := range diags {
- diagnostics = append(diagnostics, jsonDiagnostic{
- Category: f.Category,
- Posn: fset.Position(f.Pos).String(),
- Message: f.Message,
- })
+ 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,
+ })
+ }
+ jdiag := JSONDiagnostic{
+ Category: f.Category,
+ Posn: fset.Position(f.Pos).String(),
+ Message: f.Message,
+ SuggestedFixes: fixes,
+ }
+ diagnostics = append(diagnostics, jdiag)
}
v = diagnostics
}
diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go
index 7e5b848..a2393b7 100644
--- a/go/analysis/unitchecker/unitchecker_test.go
+++ b/go/analysis/unitchecker/unitchecker_test.go
@@ -20,6 +20,7 @@
"strings"
"testing"
+ "golang.org/x/tools/go/analysis/passes/assign"
"golang.org/x/tools/go/analysis/passes/findcall"
"golang.org/x/tools/go/analysis/passes/printf"
"golang.org/x/tools/go/analysis/unitchecker"
@@ -41,6 +42,7 @@
unitchecker.Main(
findcall.Analyzer,
printf.Analyzer,
+ assign.Analyzer,
)
}
@@ -75,6 +77,13 @@
func MyFunc123() {}
`,
+ "c/c.go": `package c
+
+func _() {
+ i := 5
+ i = i
+}
+`,
}}})
defer exported.Cleanup()
@@ -85,19 +94,59 @@
([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?b/b.go:6:13: call of MyFunc123\(...\)
([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?b/b.go:7:11: call of MyFunc123\(...\)
`
+ const wantC = `# golang.org/fake/c
+([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go:5:5: self-assignment of i to i
+`
const wantAJSON = `# golang.org/fake/a
\{
"golang.org/fake/a": \{
"findcall": \[
\{
"posn": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?a/a.go:4:11",
- "message": "call of MyFunc123\(...\)"
+ "message": "call of MyFunc123\(...\)",
+ "suggested_fixes": \[
+ \{
+ "message": "Add '_TEST_'",
+ "edits": \[
+ \{
+ "filename": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?a/a.go",
+ "start": 32,
+ "end": 32,
+ "new": "_TEST_"
+ \}
+ \]
+ \}
+ \]
\}
\]
\}
\}
`
-
+ const wantCJSON = `# golang.org/fake/c
+\{
+ "golang.org/fake/c": \{
+ "assign": \[
+ \{
+ "posn": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go:5:5",
+ "message": "self-assignment of i to i",
+ "suggested_fixes": \[
+ \{
+ "message": "Remove",
+ "edits": \[
+ \{
+ "filename": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go",
+ "start": 37,
+ "end": 42,
+ "new": ""
+ \}
+ \]
+ \}
+ \]
+ \}
+ \]
+ \}
+\}
+`
for _, test := range []struct {
args string
wantOut string
@@ -105,8 +154,10 @@
}{
{args: "golang.org/fake/a", wantOut: wantA, wantExit: 2},
{args: "golang.org/fake/b", wantOut: wantB, wantExit: 2},
+ {args: "golang.org/fake/c", wantOut: wantC, wantExit: 2},
{args: "golang.org/fake/a golang.org/fake/b", wantOut: wantA + wantB, wantExit: 2},
{args: "-json golang.org/fake/a", wantOut: wantAJSON, wantExit: 0},
+ {args: "-json golang.org/fake/c", wantOut: wantCJSON, wantExit: 0},
{args: "-c=0 golang.org/fake/a", wantOut: wantA + "4 MyFunc123\\(\\)\n", wantExit: 2},
} {
cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "-findcall.name=MyFunc123")
@@ -125,7 +176,7 @@
matched, err := regexp.Match(test.wantOut, out)
if err != nil {
- t.Fatal(err)
+ t.Fatalf("regexp.Match(<<%s>>): %v", test.wantOut, err)
}
if !matched {
t.Errorf("%s: got <<%s>>, want match of regexp <<%s>>", test.args, out, test.wantOut)