@@ -13,253 +13,77 @@ import (
13
13
"encoding/json"
14
14
"flag"
15
15
"fmt"
16
- "io"
17
- "io/ioutil"
18
16
"log"
19
17
"os"
20
18
"os/exec"
21
- "path/filepath"
22
- "strings"
23
19
20
+ "github.com/google/go-cmp/cmp"
24
21
"golang.org/x/tools/gopls/internal/lsp/source"
25
- diffpkg "golang.org/x/tools/internal/diff"
26
- "golang.org/x/tools/internal/gocommand"
27
22
)
28
23
29
- var (
30
- previousVersionFlag = flag .String ("prev" , "" , "version to compare against" )
31
- versionFlag = flag .String ("version" , "" , "version being tagged, or current version if omitted" )
32
- )
24
+ const usage = `api-diff <previous version> [<current version>]
25
+
26
+ Compare the API of two gopls versions. If the second argument is provided, it
27
+ will be used as the new version to compare against. Otherwise, compare against
28
+ the current API.
29
+ `
33
30
34
31
func main () {
35
32
flag .Parse ()
36
33
37
- apiDiff , err := diffAPI (* versionFlag , * previousVersionFlag )
34
+ if flag .NArg () < 1 || flag .NArg () > 2 {
35
+ fmt .Fprint (os .Stderr , usage )
36
+ os .Exit (2 )
37
+ }
38
+
39
+ oldVer := flag .Arg (0 )
40
+ newVer := ""
41
+ if flag .NArg () == 2 {
42
+ newVer = flag .Arg (1 )
43
+ }
44
+
45
+ apiDiff , err := diffAPI (oldVer , newVer )
38
46
if err != nil {
39
47
log .Fatal (err )
40
48
}
41
- fmt .Printf (`
42
- %s
43
- ` , apiDiff )
44
- }
45
-
46
- type JSON interface {
47
- String () string
48
- Write (io.Writer )
49
+ fmt .Println ("\n " + apiDiff )
49
50
}
50
51
51
- func diffAPI (version , prev string ) (string , error ) {
52
+ func diffAPI (oldVer , newVer string ) (string , error ) {
52
53
ctx := context .Background ()
53
- previousApi , err := loadAPI (ctx , prev )
54
+ previousAPI , err := loadAPI (ctx , oldVer )
54
55
if err != nil {
55
- return "" , fmt .Errorf ("load previous API : %v" , err )
56
+ return "" , fmt .Errorf ("loading %s : %v" , oldVer , err )
56
57
}
57
- var currentApi * source.APIJSON
58
- if version == "" {
59
- currentApi = source .GeneratedAPIJSON
58
+ var currentAPI * source.APIJSON
59
+ if newVer == "" {
60
+ currentAPI = source .GeneratedAPIJSON
60
61
} else {
61
62
var err error
62
- currentApi , err = loadAPI (ctx , version )
63
+ currentAPI , err = loadAPI (ctx , newVer )
63
64
if err != nil {
64
- return "" , fmt .Errorf ("load current API : %v" , err )
65
+ return "" , fmt .Errorf ("loading %s : %v" , newVer , err )
65
66
}
66
67
}
67
68
68
- b := & strings.Builder {}
69
- if err := diff (b , previousApi .Commands , currentApi .Commands , "command" , func (c * source.CommandJSON ) string {
70
- return c .Command
71
- }, diffCommands ); err != nil {
72
- return "" , fmt .Errorf ("diff commands: %v" , err )
73
- }
74
- if diff (b , previousApi .Analyzers , currentApi .Analyzers , "analyzer" , func (a * source.AnalyzerJSON ) string {
75
- return a .Name
76
- }, diffAnalyzers ); err != nil {
77
- return "" , fmt .Errorf ("diff analyzers: %v" , err )
78
- }
79
- if err := diff (b , previousApi .Lenses , currentApi .Lenses , "code lens" , func (l * source.LensJSON ) string {
80
- return l .Lens
81
- }, diffLenses ); err != nil {
82
- return "" , fmt .Errorf ("diff lenses: %v" , err )
83
- }
84
- for key , prev := range previousApi .Options {
85
- current , ok := currentApi .Options [key ]
86
- if ! ok {
87
- panic (fmt .Sprintf ("unexpected option key: %s" , key ))
88
- }
89
- if err := diff (b , prev , current , "option" , func (o * source.OptionJSON ) string {
90
- return o .Name
91
- }, diffOptions ); err != nil {
92
- return "" , fmt .Errorf ("diff options (%s): %v" , key , err )
93
- }
94
- }
95
-
96
- return b .String (), nil
69
+ return cmp .Diff (previousAPI , currentAPI ), nil
97
70
}
98
71
99
- func diff [T JSON ](b * strings.Builder , previous , new []T , kind string , uniqueKey func (T ) string , diffFunc func (* strings.Builder , T , T )) error {
100
- prevJSON := collect (previous , uniqueKey )
101
- newJSON := collect (new , uniqueKey )
102
- for k := range newJSON {
103
- delete (prevJSON , k )
104
- }
105
- for _ , deleted := range prevJSON {
106
- b .WriteString (fmt .Sprintf ("%s %s was deleted.\n " , kind , deleted ))
107
- }
108
- for _ , prev := range previous {
109
- delete (newJSON , uniqueKey (prev ))
110
- }
111
- if len (newJSON ) > 0 {
112
- b .WriteString ("The following commands were added:\n " )
113
- for _ , n := range newJSON {
114
- n .Write (b )
115
- b .WriteByte ('\n' )
116
- }
117
- }
118
- previousMap := collect (previous , uniqueKey )
119
- for _ , current := range new {
120
- prev , ok := previousMap [uniqueKey (current )]
121
- if ! ok {
122
- continue
123
- }
124
- c , p := bytes .NewBuffer (nil ), bytes .NewBuffer (nil )
125
- prev .Write (p )
126
- current .Write (c )
127
- if diff := diffStr (p .String (), c .String ()); diff != "" {
128
- diffFunc (b , prev , current )
129
- b .WriteString ("\n --\n " )
130
- }
131
- }
132
- return nil
133
- }
134
-
135
- func collect [T JSON ](args []T , uniqueKey func (T ) string ) map [string ]T {
136
- m := map [string ]T {}
137
- for _ , arg := range args {
138
- m [uniqueKey (arg )] = arg
139
- }
140
- return m
141
- }
142
-
143
- var goCmdRunner = gocommand.Runner {}
144
-
145
72
func loadAPI (ctx context.Context , version string ) (* source.APIJSON , error ) {
146
- tmpGopath , err := ioutil .TempDir ("" , "gopath*" )
147
- if err != nil {
148
- return nil , fmt .Errorf ("temp dir: %v" , err )
149
- }
150
- defer os .RemoveAll (tmpGopath )
73
+ ver := fmt .Sprintf ("golang.org/x/tools/gopls@%s" , version )
74
+ cmd := exec .Command ("go" , "run" , ver , "api-json" )
151
75
152
- exampleDir := fmt . Sprintf ( "%s/src/example.com" , tmpGopath )
153
- if err := os . MkdirAll ( exampleDir , 0776 ); err != nil {
154
- return nil , fmt . Errorf ( "mkdir: %v" , err )
155
- }
76
+ stdout := & bytes. Buffer {}
77
+ stderr := & bytes. Buffer {}
78
+ cmd . Stdout = stdout
79
+ cmd . Stderr = stderr
156
80
157
- if stdout , err := goCmdRunner .Run (ctx , gocommand.Invocation {
158
- Verb : "mod" ,
159
- Args : []string {"init" , "example.com" },
160
- WorkingDir : exampleDir ,
161
- Env : append (os .Environ (), fmt .Sprintf ("GOPATH=%s" , tmpGopath )),
162
- }); err != nil {
163
- return nil , fmt .Errorf ("go mod init failed: %v (stdout: %v)" , err , stdout )
164
- }
165
- if stdout , err := goCmdRunner .Run (ctx , gocommand.Invocation {
166
- Verb : "install" ,
167
- Args : []string {fmt .Sprintf ("golang.org/x/tools/gopls@%s" , version )},
168
- WorkingDir : exampleDir ,
169
- Env : append (os .Environ (), fmt .Sprintf ("GOPATH=%s" , tmpGopath )),
170
- }); err != nil {
171
- return nil , fmt .Errorf ("go install failed: %v (stdout: %v)" , err , stdout .String ())
172
- }
173
- cmd := exec.Cmd {
174
- Path : filepath .Join (tmpGopath , "bin" , "gopls" ),
175
- Args : []string {"gopls" , "api-json" },
176
- Dir : tmpGopath ,
177
- }
178
- out , err := cmd .Output ()
179
- if err != nil {
180
- return nil , fmt .Errorf ("output: %v" , err )
81
+ if err := cmd .Run (); err != nil {
82
+ return nil , fmt .Errorf ("go run failed: %v; stderr:\n %s" , err , stderr )
181
83
}
182
84
apiJson := & source.APIJSON {}
183
- if err := json .Unmarshal (out , apiJson ); err != nil {
85
+ if err := json .Unmarshal (stdout . Bytes () , apiJson ); err != nil {
184
86
return nil , fmt .Errorf ("unmarshal: %v" , err )
185
87
}
186
88
return apiJson , nil
187
89
}
188
-
189
- func diffCommands (b * strings.Builder , prev , current * source.CommandJSON ) {
190
- if prev .Title != current .Title {
191
- b .WriteString (fmt .Sprintf ("Title changed from %q to %q\n " , prev .Title , current .Title ))
192
- }
193
- if prev .Doc != current .Doc {
194
- b .WriteString (fmt .Sprintf ("Documentation changed from %q to %q\n " , prev .Doc , current .Doc ))
195
- }
196
- if prev .ArgDoc != current .ArgDoc {
197
- b .WriteString ("Arguments changed from " + formatBlock (prev .ArgDoc ) + " to " + formatBlock (current .ArgDoc ))
198
- }
199
- if prev .ResultDoc != current .ResultDoc {
200
- b .WriteString ("Results changed from " + formatBlock (prev .ResultDoc ) + " to " + formatBlock (current .ResultDoc ))
201
- }
202
- }
203
-
204
- func diffAnalyzers (b * strings.Builder , previous , current * source.AnalyzerJSON ) {
205
- b .WriteString (fmt .Sprintf ("Changes to analyzer %s:\n \n " , current .Name ))
206
- if previous .Doc != current .Doc {
207
- b .WriteString (fmt .Sprintf ("Documentation changed from %q to %q\n " , previous .Doc , current .Doc ))
208
- }
209
- if previous .Default != current .Default {
210
- b .WriteString (fmt .Sprintf ("Default changed from %v to %v\n " , previous .Default , current .Default ))
211
- }
212
- }
213
-
214
- func diffLenses (b * strings.Builder , previous , current * source.LensJSON ) {
215
- b .WriteString (fmt .Sprintf ("Changes to code lens %s:\n \n " , current .Title ))
216
- if previous .Title != current .Title {
217
- b .WriteString (fmt .Sprintf ("Title changed from %q to %q\n " , previous .Title , current .Title ))
218
- }
219
- if previous .Doc != current .Doc {
220
- b .WriteString (fmt .Sprintf ("Documentation changed from %q to %q\n " , previous .Doc , current .Doc ))
221
- }
222
- }
223
-
224
- func diffOptions (b * strings.Builder , previous , current * source.OptionJSON ) {
225
- b .WriteString (fmt .Sprintf ("Changes to option %s:\n \n " , current .Name ))
226
- if previous .Doc != current .Doc {
227
- diff := diffStr (previous .Doc , current .Doc )
228
- fmt .Fprintf (b , "Documentation changed:\n %s\n " , diff )
229
- }
230
- if previous .Default != current .Default {
231
- b .WriteString (fmt .Sprintf ("Default changed from %q to %q\n " , previous .Default , current .Default ))
232
- }
233
- if previous .Hierarchy != current .Hierarchy {
234
- b .WriteString (fmt .Sprintf ("Categorization changed from %q to %q\n " , previous .Hierarchy , current .Hierarchy ))
235
- }
236
- if previous .Status != current .Status {
237
- b .WriteString (fmt .Sprintf ("Status changed from %q to %q\n " , previous .Status , current .Status ))
238
- }
239
- if previous .Type != current .Type {
240
- b .WriteString (fmt .Sprintf ("Type changed from %q to %q\n " , previous .Type , current .Type ))
241
- }
242
- // TODO(rstambler): Handle possibility of same number but different keys/values.
243
- if len (previous .EnumKeys .Keys ) != len (current .EnumKeys .Keys ) {
244
- b .WriteString (fmt .Sprintf ("Enum keys changed from\n %s\n to \n %s\n " , previous .EnumKeys , current .EnumKeys ))
245
- }
246
- if len (previous .EnumValues ) != len (current .EnumValues ) {
247
- b .WriteString (fmt .Sprintf ("Enum values changed from\n %s\n to \n %s\n " , previous .EnumValues , current .EnumValues ))
248
- }
249
- }
250
-
251
- func formatBlock (str string ) string {
252
- if str == "" {
253
- return `""`
254
- }
255
- return "\n ```\n " + str + "\n ```\n "
256
- }
257
-
258
- func diffStr (before , after string ) string {
259
- if before == after {
260
- return ""
261
- }
262
- // Add newlines to avoid newline messages in diff.
263
- unified := diffpkg .Unified ("previous" , "current" , before + "\n " , after + "\n " )
264
- return fmt .Sprintf ("%q" , unified )
265
- }
0 commit comments