1
0
mirror of https://github.com/golang/go synced 2024-09-30 22:48:32 -06:00

go/analysis/analysistest: support testing mutually exclusive suggested fixes

Fixes golang/go#38431.

Change-Id: Ib2e389a7a657b8ff80995972cd3bcbcb937aeabb
Reviewed-on: https://go-review.googlesource.com/c/tools/+/229257
Run-TryBot: Dominik Honnef <dominik@honnef.co>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Michael Matloob <matloob@golang.org>
This commit is contained in:
Dominik Honnef 2020-04-21 16:20:04 +02:00
parent 655248709e
commit 4697a2867c

View File

@ -2,6 +2,7 @@
package analysistest
import (
"bytes"
"fmt"
"go/format"
"go/token"
@ -23,6 +24,7 @@ import (
"golang.org/x/tools/internal/lsp/diff/myers"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/txtar"
)
// WriteFiles is a helper function that creates a temporary directory
@ -65,10 +67,38 @@ type Testing interface {
Errorf(format string, args ...interface{})
}
// RunWithSuggestedFixes behaves like Run, but additionally verifies suggested fixes.
// It uses golden files placed alongside the source code under analysis:
// suggested fixes for code in example.go will be compared against example.go.golden.
//
// Golden files can be formatted in one of two ways: as plain Go source code, or as txtar archives.
// In the first case, all suggested fixes will be applied to the original source, which will then be compared against the golden file.
// In the second case, suggested fixes will be grouped by their messages, and each set of fixes will be applied and tested separately.
// Each section in the archive corresponds to a single message.
//
// A golden file using txtar may look like this:
// -- turn into single negation --
// package pkg
//
// func fn(b1, b2 bool) {
// if !b1 { // want `negating a boolean twice`
// println()
// }
// }
//
// -- remove double negation --
// package pkg
//
// func fn(b1, b2 bool) {
// if b1 { // want `negating a boolean twice`
// println()
// }
// }
func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result {
r := Run(t, dir, a, patterns...)
fileEdits := make(map[*token.File][]diff.TextEdit)
// file -> message -> edits
fileEdits := make(map[*token.File]map[string][]diff.TextEdit)
fileContents := make(map[*token.File][]byte)
// Validate edits, prepare the fileEdits map and read the file contents.
@ -101,7 +131,11 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
if err != nil {
t.Errorf("error converting edit to span %s: %v", file.Name(), err)
}
fileEdits[file] = append(fileEdits[file], diff.TextEdit{
if _, ok := fileEdits[file]; !ok {
fileEdits[file] = make(map[string][]diff.TextEdit)
}
fileEdits[file][sf.Message] = append(fileEdits[file][sf.Message], diff.TextEdit{
Span: spn,
NewText: string(edit.NewText),
})
@ -110,27 +144,77 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
}
}
for file, edits := range fileEdits {
for file, fixes := range fileEdits {
// Get the original file contents.
orig, ok := fileContents[file]
if !ok {
t.Errorf("could not find file contents for %s", file.Name())
continue
}
out := diff.ApplyEdits(string(orig), edits)
// Get the golden file and read the contents.
want, err := ioutil.ReadFile(file.Name() + ".golden")
ar, err := txtar.ParseFile(file.Name() + ".golden")
if err != nil {
t.Errorf("error reading %s.golden: %v", file.Name(), err)
continue
}
formatted, err := format.Source([]byte(out))
if err != nil {
continue
}
if string(want) != string(formatted) {
d := myers.ComputeEdits("", string(want), string(formatted))
t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(file.Name()+".golden", "actual", string(want), d))
if len(ar.Files) > 0 {
// one virtual file per kind of suggested fix
if len(ar.Comment) != 0 {
// we allow either just the comment, or just virtual
// files, not both. it is not clear how "both" should
// behave.
t.Errorf("%s.golden has leading comment; we don't know what to do with it", file.Name())
continue
}
for sf, edits := range fixes {
found := false
for _, vf := range ar.Files {
if vf.Name == sf {
found = true
out := diff.ApplyEdits(string(orig), edits)
// the file may contain multiple trailing
// newlines if the user places empty lines
// between files in the archive. normalize
// this to a single newline.
want := string(bytes.TrimRight(vf.Data, "\n")) + "\n"
formatted, err := format.Source([]byte(out))
if err != nil {
continue
}
if want != string(formatted) {
d := myers.ComputeEdits("", want, string(formatted))
t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, d))
}
break
}
}
if !found {
t.Errorf("no section for suggested fix %q in %s.golden", sf, file.Name())
}
}
} else {
// all suggested fixes are represented by a single file
var catchallEdits []diff.TextEdit
for _, edits := range fixes {
catchallEdits = append(catchallEdits, edits...)
}
out := diff.ApplyEdits(string(orig), catchallEdits)
want := string(ar.Comment)
formatted, err := format.Source([]byte(out))
if err != nil {
continue
}
if want != string(formatted) {
d := myers.ComputeEdits("", want, string(formatted))
t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(file.Name()+".golden", "actual", want, d))
}
}
}
return r