mirror of https://github.com/golang/go.git
275 lines
8.2 KiB
Go
275 lines
8.2 KiB
Go
// Copyright 2021 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//go:build go1.18
|
|
// +build go1.18
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/internal/gocommand"
|
|
difflib "golang.org/x/tools/internal/lsp/diff"
|
|
"golang.org/x/tools/internal/lsp/diff/myers"
|
|
"golang.org/x/tools/internal/lsp/source"
|
|
)
|
|
|
|
var (
|
|
previousVersionFlag = flag.String("prev", "", "version to compare against")
|
|
versionFlag = flag.String("version", "", "version being tagged, or current version if omitted")
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
apiDiff, err := diffAPI(*versionFlag, *previousVersionFlag)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Printf(`
|
|
%s
|
|
`, apiDiff)
|
|
}
|
|
|
|
type JSON interface {
|
|
String() string
|
|
Write(io.Writer)
|
|
}
|
|
|
|
func diffAPI(version, prev string) (string, error) {
|
|
ctx := context.Background()
|
|
previousApi, err := loadAPI(ctx, prev)
|
|
if err != nil {
|
|
return "", fmt.Errorf("load previous API: %v", err)
|
|
}
|
|
var currentApi *source.APIJSON
|
|
if version == "" {
|
|
currentApi = source.GeneratedAPIJSON
|
|
} else {
|
|
var err error
|
|
currentApi, err = loadAPI(ctx, version)
|
|
if err != nil {
|
|
return "", fmt.Errorf("load current API: %v", err)
|
|
}
|
|
}
|
|
|
|
b := &strings.Builder{}
|
|
if err := diff(b, previousApi.Commands, currentApi.Commands, "command", func(c *source.CommandJSON) string {
|
|
return c.Command
|
|
}, diffCommands); err != nil {
|
|
return "", fmt.Errorf("diff commands: %v", err)
|
|
}
|
|
if diff(b, previousApi.Analyzers, currentApi.Analyzers, "analyzer", func(a *source.AnalyzerJSON) string {
|
|
return a.Name
|
|
}, diffAnalyzers); err != nil {
|
|
return "", fmt.Errorf("diff analyzers: %v", err)
|
|
}
|
|
if err := diff(b, previousApi.Lenses, currentApi.Lenses, "code lens", func(l *source.LensJSON) string {
|
|
return l.Lens
|
|
}, diffLenses); err != nil {
|
|
return "", fmt.Errorf("diff lenses: %v", err)
|
|
}
|
|
for key, prev := range previousApi.Options {
|
|
current, ok := currentApi.Options[key]
|
|
if !ok {
|
|
panic(fmt.Sprintf("unexpected option key: %s", key))
|
|
}
|
|
if err := diff(b, prev, current, "option", func(o *source.OptionJSON) string {
|
|
return o.Name
|
|
}, diffOptions); err != nil {
|
|
return "", fmt.Errorf("diff options (%s): %v", key, err)
|
|
}
|
|
}
|
|
|
|
return b.String(), nil
|
|
}
|
|
|
|
func diff[T JSON](b *strings.Builder, previous, new []T, kind string, uniqueKey func(T) string, diffFunc func(*strings.Builder, T, T)) error {
|
|
prevJSON := collect(previous, uniqueKey)
|
|
newJSON := collect(new, uniqueKey)
|
|
for k := range newJSON {
|
|
delete(prevJSON, k)
|
|
}
|
|
for _, deleted := range prevJSON {
|
|
b.WriteString(fmt.Sprintf("%s %s was deleted.\n", kind, deleted))
|
|
}
|
|
for _, prev := range previous {
|
|
delete(newJSON, uniqueKey(prev))
|
|
}
|
|
if len(newJSON) > 0 {
|
|
b.WriteString("The following commands were added:\n")
|
|
for _, n := range newJSON {
|
|
n.Write(b)
|
|
b.WriteByte('\n')
|
|
}
|
|
}
|
|
previousMap := collect(previous, uniqueKey)
|
|
for _, current := range new {
|
|
prev, ok := previousMap[uniqueKey(current)]
|
|
if !ok {
|
|
continue
|
|
}
|
|
c, p := bytes.NewBuffer(nil), bytes.NewBuffer(nil)
|
|
prev.Write(p)
|
|
current.Write(c)
|
|
if diff, err := diffStr(p.String(), c.String()); err == nil && diff != "" {
|
|
diffFunc(b, prev, current)
|
|
b.WriteString("\n--\n")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func collect[T JSON](args []T, uniqueKey func(T) string) map[string]T {
|
|
m := map[string]T{}
|
|
for _, arg := range args {
|
|
m[uniqueKey(arg)] = arg
|
|
}
|
|
return m
|
|
}
|
|
|
|
var goCmdRunner = gocommand.Runner{}
|
|
|
|
func loadAPI(ctx context.Context, version string) (*source.APIJSON, error) {
|
|
tmpGopath, err := ioutil.TempDir("", "gopath*")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpGopath)
|
|
|
|
exampleDir := fmt.Sprintf("%s/src/example.com", tmpGopath)
|
|
if err := os.MkdirAll(exampleDir, 0776); err != nil {
|
|
return nil, fmt.Errorf("mkdir: %v", err)
|
|
}
|
|
|
|
if stdout, err := goCmdRunner.Run(ctx, gocommand.Invocation{
|
|
Verb: "mod",
|
|
Args: []string{"init", "example.com"},
|
|
WorkingDir: exampleDir,
|
|
Env: append(os.Environ(), fmt.Sprintf("GOPATH=%s", tmpGopath)),
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("go mod init failed: %v (stdout: %v)", err, stdout)
|
|
}
|
|
if stdout, err := goCmdRunner.Run(ctx, gocommand.Invocation{
|
|
Verb: "install",
|
|
Args: []string{fmt.Sprintf("golang.org/x/tools/gopls@%s", version)},
|
|
WorkingDir: exampleDir,
|
|
Env: append(os.Environ(), fmt.Sprintf("GOPATH=%s", tmpGopath)),
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("go install failed: %v (stdout: %v)", err, stdout.String())
|
|
}
|
|
cmd := exec.Cmd{
|
|
Path: filepath.Join(tmpGopath, "bin", "gopls"),
|
|
Args: []string{"gopls", "api-json"},
|
|
Dir: tmpGopath,
|
|
}
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("output: %v", err)
|
|
}
|
|
apiJson := &source.APIJSON{}
|
|
if err := json.Unmarshal(out, apiJson); err != nil {
|
|
return nil, fmt.Errorf("unmarshal: %v", err)
|
|
}
|
|
return apiJson, nil
|
|
}
|
|
|
|
func diffCommands(b *strings.Builder, prev, current *source.CommandJSON) {
|
|
if prev.Title != current.Title {
|
|
b.WriteString(fmt.Sprintf("Title changed from %q to %q\n", prev.Title, current.Title))
|
|
}
|
|
if prev.Doc != current.Doc {
|
|
b.WriteString(fmt.Sprintf("Documentation changed from %q to %q\n", prev.Doc, current.Doc))
|
|
}
|
|
if prev.ArgDoc != current.ArgDoc {
|
|
b.WriteString("Arguments changed from " + formatBlock(prev.ArgDoc) + " to " + formatBlock(current.ArgDoc))
|
|
}
|
|
if prev.ResultDoc != current.ResultDoc {
|
|
b.WriteString("Results changed from " + formatBlock(prev.ResultDoc) + " to " + formatBlock(current.ResultDoc))
|
|
}
|
|
}
|
|
|
|
func diffAnalyzers(b *strings.Builder, previous, current *source.AnalyzerJSON) {
|
|
b.WriteString(fmt.Sprintf("Changes to analyzer %s:\n\n", current.Name))
|
|
if previous.Doc != current.Doc {
|
|
b.WriteString(fmt.Sprintf("Documentation changed from %q to %q\n", previous.Doc, current.Doc))
|
|
}
|
|
if previous.Default != current.Default {
|
|
b.WriteString(fmt.Sprintf("Default changed from %v to %v\n", previous.Default, current.Default))
|
|
}
|
|
}
|
|
|
|
func diffLenses(b *strings.Builder, previous, current *source.LensJSON) {
|
|
b.WriteString(fmt.Sprintf("Changes to code lens %s:\n\n", current.Title))
|
|
if previous.Title != current.Title {
|
|
b.WriteString(fmt.Sprintf("Title changed from %q to %q\n", previous.Title, current.Title))
|
|
}
|
|
if previous.Doc != current.Doc {
|
|
b.WriteString(fmt.Sprintf("Documentation changed from %q to %q\n", previous.Doc, current.Doc))
|
|
}
|
|
}
|
|
|
|
func diffOptions(b *strings.Builder, previous, current *source.OptionJSON) {
|
|
b.WriteString(fmt.Sprintf("Changes to option %s:\n\n", current.Name))
|
|
if previous.Doc != current.Doc {
|
|
diff, err := diffStr(previous.Doc, current.Doc)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
b.WriteString(fmt.Sprintf("Documentation changed:\n%s\n", diff))
|
|
}
|
|
if previous.Default != current.Default {
|
|
b.WriteString(fmt.Sprintf("Default changed from %q to %q\n", previous.Default, current.Default))
|
|
}
|
|
if previous.Hierarchy != current.Hierarchy {
|
|
b.WriteString(fmt.Sprintf("Categorization changed from %q to %q\n", previous.Hierarchy, current.Hierarchy))
|
|
}
|
|
if previous.Status != current.Status {
|
|
b.WriteString(fmt.Sprintf("Status changed from %q to %q\n", previous.Status, current.Status))
|
|
}
|
|
if previous.Type != current.Type {
|
|
b.WriteString(fmt.Sprintf("Type changed from %q to %q\n", previous.Type, current.Type))
|
|
}
|
|
// TODO(rstambler): Handle possibility of same number but different keys/values.
|
|
if len(previous.EnumKeys.Keys) != len(current.EnumKeys.Keys) {
|
|
b.WriteString(fmt.Sprintf("Enum keys changed from\n%s\n to \n%s\n", previous.EnumKeys, current.EnumKeys))
|
|
}
|
|
if len(previous.EnumValues) != len(current.EnumValues) {
|
|
b.WriteString(fmt.Sprintf("Enum values changed from\n%s\n to \n%s\n", previous.EnumValues, current.EnumValues))
|
|
}
|
|
}
|
|
|
|
func formatBlock(str string) string {
|
|
if str == "" {
|
|
return `""`
|
|
}
|
|
return "\n```\n" + str + "\n```\n"
|
|
}
|
|
|
|
func diffStr(before, after string) (string, error) {
|
|
// Add newlines to avoid newline messages in diff.
|
|
if before == after {
|
|
return "", nil
|
|
}
|
|
before += "\n"
|
|
after += "\n"
|
|
d, err := myers.ComputeEdits("", before, after)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%q", difflib.ToUnified("previous", "current", before, d)), err
|
|
}
|