Skip to content

Commit 70a130e

Browse files
committed
gopls/api-diff: simplify the api-diff implementation
Simplify the api-diff implementation to use `go run` and cmp.Diff. The latter is more maintainable and produces more readable output, due to supporting line diffs for multi-line strings. For golang/go#54459 Change-Id: I11c00e9728ce241aef8f9828f3840b4202294a9a Reviewed-on: https://go-review.googlesource.com/c/tools/+/444799 TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Robert Findley <rfindley@google.com> Reviewed-by: Alan Donovan <adonovan@google.com> gopls-CI: kokoro <noreply+kokoro@google.com>
1 parent 3e8da47 commit 70a130e

File tree

1 file changed

+38
-214
lines changed

1 file changed

+38
-214
lines changed

gopls/api-diff/api_diff.go

+38-214
Original file line numberDiff line numberDiff line change
@@ -13,253 +13,77 @@ import (
1313
"encoding/json"
1414
"flag"
1515
"fmt"
16-
"io"
17-
"io/ioutil"
1816
"log"
1917
"os"
2018
"os/exec"
21-
"path/filepath"
22-
"strings"
2319

20+
"github.com/google/go-cmp/cmp"
2421
"golang.org/x/tools/gopls/internal/lsp/source"
25-
diffpkg "golang.org/x/tools/internal/diff"
26-
"golang.org/x/tools/internal/gocommand"
2722
)
2823

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+
`
3330

3431
func main() {
3532
flag.Parse()
3633

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)
3846
if err != nil {
3947
log.Fatal(err)
4048
}
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)
4950
}
5051

51-
func diffAPI(version, prev string) (string, error) {
52+
func diffAPI(oldVer, newVer string) (string, error) {
5253
ctx := context.Background()
53-
previousApi, err := loadAPI(ctx, prev)
54+
previousAPI, err := loadAPI(ctx, oldVer)
5455
if err != nil {
55-
return "", fmt.Errorf("load previous API: %v", err)
56+
return "", fmt.Errorf("loading %s: %v", oldVer, err)
5657
}
57-
var currentApi *source.APIJSON
58-
if version == "" {
59-
currentApi = source.GeneratedAPIJSON
58+
var currentAPI *source.APIJSON
59+
if newVer == "" {
60+
currentAPI = source.GeneratedAPIJSON
6061
} else {
6162
var err error
62-
currentApi, err = loadAPI(ctx, version)
63+
currentAPI, err = loadAPI(ctx, newVer)
6364
if err != nil {
64-
return "", fmt.Errorf("load current API: %v", err)
65+
return "", fmt.Errorf("loading %s: %v", newVer, err)
6566
}
6667
}
6768

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
9770
}
9871

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-
14572
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")
15175

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
15680

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)
18183
}
18284
apiJson := &source.APIJSON{}
183-
if err := json.Unmarshal(out, apiJson); err != nil {
85+
if err := json.Unmarshal(stdout.Bytes(), apiJson); err != nil {
18486
return nil, fmt.Errorf("unmarshal: %v", err)
18587
}
18688
return apiJson, nil
18789
}
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

Comments
 (0)