Skip to content

Commit b1df990

Browse files
committed
go/analysis/analysistest: expand testing to handle suggested fixes
This change expands go/analysis to add the ability to verify the suggested fixes returned by an analyzer. Change-Id: Ic38e1a24342a5c24356f8b83d196da012d8e8e01 Reviewed-on: https://go-review.googlesource.com/c/tools/+/224959 Reviewed-by: Michael Matloob <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]> Run-TryBot: Rohan Challa <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 02a6ca6 commit b1df990

File tree

10 files changed

+203
-12
lines changed

10 files changed

+203
-12
lines changed

go/analysis/analysistest/analysistest.go

+72
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package analysistest
33

44
import (
55
"fmt"
6+
"go/format"
67
"go/token"
78
"go/types"
89
"io/ioutil"
@@ -18,6 +19,8 @@ import (
1819
"golang.org/x/tools/go/analysis"
1920
"golang.org/x/tools/go/analysis/internal/checker"
2021
"golang.org/x/tools/go/packages"
22+
"golang.org/x/tools/internal/lsp/diff"
23+
"golang.org/x/tools/internal/span"
2124
"golang.org/x/tools/internal/testenv"
2225
)
2326

@@ -61,6 +64,75 @@ type Testing interface {
6164
Errorf(format string, args ...interface{})
6265
}
6366

67+
func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result {
68+
r := Run(t, dir, a, patterns...)
69+
70+
fileEdits := make(map[*token.File][]diff.TextEdit)
71+
fileContents := make(map[*token.File][]byte)
72+
73+
// Validate edits, prepare the fileEdits map and read the file contents.
74+
for _, act := range r {
75+
for _, diag := range act.Diagnostics {
76+
for _, sf := range diag.SuggestedFixes {
77+
for _, edit := range sf.TextEdits {
78+
// Validate the edit.
79+
if edit.Pos > edit.End {
80+
t.Errorf(
81+
"diagnostic for analysis %v contains Suggested Fix with malformed edit: pos (%v) > end (%v)",
82+
act.Pass.Analyzer.Name, edit.Pos, edit.End)
83+
continue
84+
}
85+
file, endfile := act.Pass.Fset.File(edit.Pos), act.Pass.Fset.File(edit.End)
86+
if file == nil || endfile == nil || file != endfile {
87+
t.Errorf(
88+
"diagnostic for analysis %v contains Suggested Fix with malformed spanning files %v and %v",
89+
act.Pass.Analyzer.Name, file.Name(), endfile.Name())
90+
continue
91+
}
92+
if _, ok := fileContents[file]; !ok {
93+
contents, err := ioutil.ReadFile(file.Name())
94+
if err != nil {
95+
t.Errorf("error reading %s: %v", file.Name(), err)
96+
}
97+
fileContents[file] = contents
98+
}
99+
spn, err := span.NewRange(act.Pass.Fset, edit.Pos, edit.End).Span()
100+
if err != nil {
101+
t.Errorf("error converting edit to span %s: %v", file.Name(), err)
102+
}
103+
fileEdits[file] = append(fileEdits[file], diff.TextEdit{
104+
Span: spn,
105+
NewText: string(edit.NewText),
106+
})
107+
}
108+
}
109+
}
110+
}
111+
112+
for file, edits := range fileEdits {
113+
// Get the original file contents.
114+
orig, ok := fileContents[file]
115+
if !ok {
116+
t.Errorf("could not find file contents for %s", file.Name())
117+
continue
118+
}
119+
out := diff.ApplyEdits(string(orig), edits)
120+
// Get the golden file and read the contents.
121+
want, err := ioutil.ReadFile(file.Name() + ".golden")
122+
if err != nil {
123+
t.Errorf("error reading %s.golden: %v", file.Name(), err)
124+
}
125+
formatted, err := format.Source([]byte(out))
126+
if err != nil {
127+
continue
128+
}
129+
if string(want) != string(formatted) {
130+
t.Errorf("suggested fixes failed for %s, expected:\n%#v\ngot:\n%#v", file.Name(), string(want), string(formatted))
131+
}
132+
}
133+
return r
134+
}
135+
64136
// Run applies an analysis to the packages denoted by the "go list" patterns.
65137
//
66138
// It loads the packages from the specified GOPATH-style project

go/analysis/analysistest/analysistest_test.go

+39-7
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,44 @@ func main() {
7070
// OK (facts and diagnostics on same line)
7171
func println(...interface{}) { println() } // want println:"found" "call of println(...)"
7272
73+
`,
74+
"a/b.go.golden": `package main // want package:"found"
75+
76+
func main() {
77+
// The expectation is ill-formed:
78+
print() // want: "diagnostic"
79+
print() // want foo"fact"
80+
print() // want foo:
81+
print() // want "\xZZ scan error"
82+
83+
// A diagnostic is reported at this line, but the expectation doesn't match:
84+
println_TEST_("hello, world") // want "wrong expectation text"
85+
86+
// An unexpected diagnostic is reported at this line:
87+
println_TEST_() // trigger an unexpected diagnostic
88+
89+
// No diagnostic is reported at this line:
90+
print() // want "unsatisfied expectation"
91+
92+
// OK
93+
println_TEST_("hello, world") // want "call of println"
94+
95+
// OK /* */-form.
96+
println_TEST_("안녕, 세계") /* want "call of println" */
97+
98+
// OK (nested comment)
99+
println_TEST_("Γειά σου, Κόσμε") // some comment // want "call of println"
100+
101+
// OK (nested comment in /**/)
102+
println_TEST_("你好,世界") /* some comment // want "call of println" */
103+
104+
// OK (multiple expectations on same line)
105+
println_TEST_()
106+
println_TEST_() // want "call of println(...)" "call of println(...)"
107+
}
108+
109+
// OK (facts and diagnostics on same line)
110+
func println(...interface{}) { println_TEST_() } // want println:"found" "call of println(...)"
73111
`}
74112
dir, cleanup, err := analysistest.WriteFiles(filemap)
75113
if err != nil {
@@ -79,7 +117,7 @@ func println(...interface{}) { println() } // want println:"found" "call of prin
79117

80118
var got []string
81119
t2 := errorfunc(func(s string) { got = append(got, s) }) // a fake *testing.T
82-
analysistest.Run(t2, dir, findcall.Analyzer, "a")
120+
analysistest.RunWithSuggestedFixes(t2, dir, findcall.Analyzer, "a")
83121

84122
want := []string{
85123
`a/b.go:5: in 'want' comment: unexpected ":"`,
@@ -91,12 +129,6 @@ func println(...interface{}) { println() } // want println:"found" "call of prin
91129
`a/b.go:11: no diagnostic was reported matching "wrong expectation text"`,
92130
`a/b.go:17: no diagnostic was reported matching "unsatisfied expectation"`,
93131
}
94-
// Go 1.13's scanner error messages uses the word invalid where Go 1.12 used illegal. Convert them
95-
// to keep tests compatible with both.
96-
// TODO(matloob): Remove this once Go 1.13 is released.
97-
for i := range got {
98-
got[i] = strings.Replace(got[i], "illegal", "invalid", -1)
99-
} //
100132
if !reflect.DeepEqual(got, want) {
101133
t.Errorf("got:\n%s\nwant:\n%s",
102134
strings.Join(got, "\n"),

go/analysis/passes/assign/assign_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ import (
1313

1414
func Test(t *testing.T) {
1515
testdata := analysistest.TestData()
16-
analysistest.Run(t, testdata, assign.Analyzer, "a")
16+
analysistest.RunWithSuggestedFixes(t, testdata, assign.Analyzer, "a")
1717
}

go/analysis/passes/assign/testdata/src/a/a.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2013 The Go Authors. All rights reserved.
1+
// Copyright 2020 The Go Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file contains tests for the useless-assignment checker.
6+
7+
package testdata
8+
9+
import "math/rand"
10+
11+
type ST struct {
12+
x int
13+
l []int
14+
}
15+
16+
func (s *ST) SetX(x int, ch chan int) {
17+
// Accidental self-assignment; it should be "s.x = x"
18+
// want "self-assignment of x to x"
19+
// Another mistake
20+
// want "self-assignment of s.x to s.x"
21+
22+
// want "self-assignment of s.l.0. to s.l.0."
23+
24+
// Bail on any potential side effects to avoid false positives
25+
s.l[num()] = s.l[num()]
26+
rng := rand.New(rand.NewSource(0))
27+
s.l[rng.Intn(len(s.l))] = s.l[rng.Intn(len(s.l))]
28+
s.l[<-ch] = s.l[<-ch]
29+
}
30+
31+
func num() int { return 2 }

go/analysis/passes/findcall/findcall.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package findcall
1212

1313
import (
14+
"fmt"
1415
"go/ast"
1516
"go/types"
1617

@@ -48,7 +49,18 @@ func run(pass *analysis.Pass) (interface{}, error) {
4849
id = fun.Sel
4950
}
5051
if id != nil && !pass.TypesInfo.Types[id].IsType() && id.Name == name {
51-
pass.Reportf(call.Lparen, "call of %s(...)", id.Name)
52+
pass.Report(analysis.Diagnostic{
53+
Pos: call.Lparen,
54+
Message: fmt.Sprintf("call of %s(...)", id.Name),
55+
SuggestedFixes: []analysis.SuggestedFix{{
56+
Message: fmt.Sprintf("Add '_TEST_'"),
57+
TextEdits: []analysis.TextEdit{{
58+
Pos: call.Lparen,
59+
End: call.Lparen,
60+
NewText: []byte("_TEST_"),
61+
}},
62+
}},
63+
})
5264
}
5365
}
5466
return true

go/analysis/passes/findcall/findcall_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,5 @@ func println(s string) {} // want println:"found"`,
6262
// multiple variants of a single scenario.
6363
func TestFromFileSystem(t *testing.T) {
6464
testdata := analysistest.TestData()
65-
analysistest.Run(t, testdata, findcall.Analyzer, "a") // loads testdata/src/a/a.go.
65+
analysistest.RunWithSuggestedFixes(t, testdata, findcall.Analyzer, "a") // loads testdata/src/a/a.go.
6666
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package main // want package:"found"
2+
3+
func main() {
4+
println_TEST_("hi") // want "call of println"
5+
print("hi") // not a call of println
6+
}
7+
8+
func println(s string) {} // want println:"found"

go/analysis/passes/stringintconv/string_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ import (
1313

1414
func Test(t *testing.T) {
1515
testdata := analysistest.TestData()
16-
analysistest.Run(t, testdata, stringintconv.Analyzer, "a")
16+
analysistest.RunWithSuggestedFixes(t, testdata, stringintconv.Analyzer, "a")
1717
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file contains tests for the stringintconv checker.
6+
7+
package a
8+
9+
type A string
10+
11+
type B = string
12+
13+
type C int
14+
15+
type D = uintptr
16+
17+
func StringTest() {
18+
var (
19+
i int
20+
j rune
21+
k byte
22+
l C
23+
m D
24+
n = []int{0, 1, 2}
25+
o struct{ x int }
26+
)
27+
const p = 0
28+
_ = string(rune(i)) // want `^conversion from int to string yields a string of one rune$`
29+
_ = string(j)
30+
_ = string(k)
31+
_ = string(rune(p)) // want `^conversion from untyped int to string yields a string of one rune$`
32+
_ = A(rune(l)) // want `^conversion from C \(int\) to A \(string\) yields a string of one rune$`
33+
_ = B(rune(m)) // want `^conversion from uintptr to B \(string\) yields a string of one rune$`
34+
_ = string(rune(n[1])) // want `^conversion from int to string yields a string of one rune$`
35+
_ = string(rune(o.x)) // want `^conversion from int to string yields a string of one rune$`
36+
}

0 commit comments

Comments
 (0)