2
2
package analysistest
3
3
4
4
import (
5
+ "bytes"
5
6
"fmt"
6
7
"go/format"
7
8
"go/token"
@@ -20,8 +21,10 @@ import (
20
21
"golang.org/x/tools/go/analysis/internal/checker"
21
22
"golang.org/x/tools/go/packages"
22
23
"golang.org/x/tools/internal/lsp/diff"
24
+ "golang.org/x/tools/internal/lsp/diff/myers"
23
25
"golang.org/x/tools/internal/span"
24
26
"golang.org/x/tools/internal/testenv"
27
+ "golang.org/x/tools/txtar"
25
28
)
26
29
27
30
// WriteFiles is a helper function that creates a temporary directory
@@ -64,10 +67,38 @@ type Testing interface {
64
67
Errorf (format string , args ... interface {})
65
68
}
66
69
70
+ // RunWithSuggestedFixes behaves like Run, but additionally verifies suggested fixes.
71
+ // It uses golden files placed alongside the source code under analysis:
72
+ // suggested fixes for code in example.go will be compared against example.go.golden.
73
+ //
74
+ // Golden files can be formatted in one of two ways: as plain Go source code, or as txtar archives.
75
+ // In the first case, all suggested fixes will be applied to the original source, which will then be compared against the golden file.
76
+ // In the second case, suggested fixes will be grouped by their messages, and each set of fixes will be applied and tested separately.
77
+ // Each section in the archive corresponds to a single message.
78
+ //
79
+ // A golden file using txtar may look like this:
80
+ // -- turn into single negation --
81
+ // package pkg
82
+ //
83
+ // func fn(b1, b2 bool) {
84
+ // if !b1 { // want `negating a boolean twice`
85
+ // println()
86
+ // }
87
+ // }
88
+ //
89
+ // -- remove double negation --
90
+ // package pkg
91
+ //
92
+ // func fn(b1, b2 bool) {
93
+ // if b1 { // want `negating a boolean twice`
94
+ // println()
95
+ // }
96
+ // }
67
97
func RunWithSuggestedFixes (t Testing , dir string , a * analysis.Analyzer , patterns ... string ) []* Result {
68
98
r := Run (t , dir , a , patterns ... )
69
99
70
- fileEdits := make (map [* token.File ][]diff.TextEdit )
100
+ // file -> message -> edits
101
+ fileEdits := make (map [* token.File ]map [string ][]diff.TextEdit )
71
102
fileContents := make (map [* token.File ][]byte )
72
103
73
104
// Validate edits, prepare the fileEdits map and read the file contents.
@@ -100,7 +131,11 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
100
131
if err != nil {
101
132
t .Errorf ("error converting edit to span %s: %v" , file .Name (), err )
102
133
}
103
- fileEdits [file ] = append (fileEdits [file ], diff.TextEdit {
134
+
135
+ if _ , ok := fileEdits [file ]; ! ok {
136
+ fileEdits [file ] = make (map [string ][]diff.TextEdit )
137
+ }
138
+ fileEdits [file ][sf.Message ] = append (fileEdits [file ][sf.Message ], diff.TextEdit {
104
139
Span : spn ,
105
140
NewText : string (edit .NewText ),
106
141
})
@@ -109,25 +144,77 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns
109
144
}
110
145
}
111
146
112
- for file , edits := range fileEdits {
147
+ for file , fixes := range fileEdits {
113
148
// Get the original file contents.
114
149
orig , ok := fileContents [file ]
115
150
if ! ok {
116
151
t .Errorf ("could not find file contents for %s" , file .Name ())
117
152
continue
118
153
}
119
- out := diff . ApplyEdits ( string ( orig ), edits )
154
+
120
155
// Get the golden file and read the contents.
121
- want , err := ioutil . ReadFile (file .Name () + ".golden" )
156
+ ar , err := txtar . ParseFile (file .Name () + ".golden" )
122
157
if err != nil {
123
158
t .Errorf ("error reading %s.golden: %v" , file .Name (), err )
124
- }
125
- formatted , err := format .Source ([]byte (out ))
126
- if err != nil {
127
159
continue
128
160
}
129
- if string (want ) != string (formatted ) {
130
- t .Errorf ("suggested fixes failed for %s, expected:\n %#v\n got:\n %#v" , file .Name (), string (want ), string (formatted ))
161
+
162
+ if len (ar .Files ) > 0 {
163
+ // one virtual file per kind of suggested fix
164
+
165
+ if len (ar .Comment ) != 0 {
166
+ // we allow either just the comment, or just virtual
167
+ // files, not both. it is not clear how "both" should
168
+ // behave.
169
+ t .Errorf ("%s.golden has leading comment; we don't know what to do with it" , file .Name ())
170
+ continue
171
+ }
172
+
173
+ for sf , edits := range fixes {
174
+ found := false
175
+ for _ , vf := range ar .Files {
176
+ if vf .Name == sf {
177
+ found = true
178
+ out := diff .ApplyEdits (string (orig ), edits )
179
+ // the file may contain multiple trailing
180
+ // newlines if the user places empty lines
181
+ // between files in the archive. normalize
182
+ // this to a single newline.
183
+ want := string (bytes .TrimRight (vf .Data , "\n " )) + "\n "
184
+ formatted , err := format .Source ([]byte (out ))
185
+ if err != nil {
186
+ continue
187
+ }
188
+ if want != string (formatted ) {
189
+ d := myers .ComputeEdits ("" , want , string (formatted ))
190
+ t .Errorf ("suggested fixes failed for %s:\n %s" , file .Name (), diff .ToUnified (fmt .Sprintf ("%s.golden [%s]" , file .Name (), sf ), "actual" , want , d ))
191
+ }
192
+ break
193
+ }
194
+ }
195
+ if ! found {
196
+ t .Errorf ("no section for suggested fix %q in %s.golden" , sf , file .Name ())
197
+ }
198
+ }
199
+ } else {
200
+ // all suggested fixes are represented by a single file
201
+
202
+ var catchallEdits []diff.TextEdit
203
+ for _ , edits := range fixes {
204
+ catchallEdits = append (catchallEdits , edits ... )
205
+ }
206
+
207
+ out := diff .ApplyEdits (string (orig ), catchallEdits )
208
+ want := string (ar .Comment )
209
+
210
+ formatted , err := format .Source ([]byte (out ))
211
+ if err != nil {
212
+ continue
213
+ }
214
+ if want != string (formatted ) {
215
+ d := myers .ComputeEdits ("" , want , string (formatted ))
216
+ t .Errorf ("suggested fixes failed for %s:\n %s" , file .Name (), diff .ToUnified (file .Name ()+ ".golden" , "actual" , want , d ))
217
+ }
131
218
}
132
219
}
133
220
return r
0 commit comments