Skip to content

Commit 2758b04

Browse files
committed
gopls/api-diff: create api-diff command for gopls api
This change adds a command that can be used to see the difference in API between two gopls versions. It prints out the changes in a way that can be copy-pasted into the release notes. Also, only run the copyright test with 1.18. I wanted to do this before to use filepath.WalkDir, but now it also doesn't work with generic syntax (it doesn't use packages.Load, so doesn't respect build tags). Fixes golang/go#46652 Change-Id: I3670e0289a8eeaca02f4dcd8f88f206796ed2462 Reviewed-on: https://go-review.googlesource.com/c/tools/+/327276 Trust: Rebecca Stambler <[email protected]> Run-TryBot: Rebecca Stambler <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent aba0c5f commit 2758b04

File tree

6 files changed

+377
-65
lines changed

6 files changed

+377
-65
lines changed

copyright/copyright.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,37 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.18
6+
// +build go1.18
7+
58
// Package copyright checks that files have the correct copyright notices.
69
package copyright
710

811
import (
912
"go/ast"
1013
"go/parser"
1114
"go/token"
15+
"io/fs"
1216
"io/ioutil"
13-
"os"
1417
"path/filepath"
1518
"regexp"
1619
"strings"
1720
)
1821

1922
func checkCopyright(dir string) ([]string, error) {
2023
var files []string
21-
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
24+
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
2225
if err != nil {
2326
return err
2427
}
25-
if info.IsDir() {
28+
if d.IsDir() {
2629
// Skip directories like ".git".
27-
if strings.HasPrefix(info.Name(), ".") {
30+
if strings.HasPrefix(d.Name(), ".") {
31+
return filepath.SkipDir
32+
}
33+
// Skip any directory that starts with an underscore, as the go
34+
// command would.
35+
if strings.HasPrefix(d.Name(), "_") {
2836
return filepath.SkipDir
2937
}
3038
return nil

copyright/copyright_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.18
6+
// +build go1.18
7+
58
package copyright
69

710
import (

gopls/api-diff/api_diff.go

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

0 commit comments

Comments
 (0)