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 <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/copyright/copyright.go b/copyright/copyright.go
index a20d623..4a04d13 100644
--- a/copyright/copyright.go
+++ b/copyright/copyright.go
@@ -2,6 +2,9 @@
// 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 copyright checks that files have the correct copyright notices.
package copyright
@@ -9,8 +12,8 @@
"go/ast"
"go/parser"
"go/token"
+ "io/fs"
"io/ioutil"
- "os"
"path/filepath"
"regexp"
"strings"
@@ -18,13 +21,18 @@
func checkCopyright(dir string) ([]string, error) {
var files []string
- err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
- if info.IsDir() {
+ if d.IsDir() {
// Skip directories like ".git".
- if strings.HasPrefix(info.Name(), ".") {
+ if strings.HasPrefix(d.Name(), ".") {
+ return filepath.SkipDir
+ }
+ // Skip any directory that starts with an underscore, as the go
+ // command would.
+ if strings.HasPrefix(d.Name(), "_") {
return filepath.SkipDir
}
return nil
diff --git a/copyright/copyright_test.go b/copyright/copyright_test.go
index bfab43c..1d63147 100644
--- a/copyright/copyright_test.go
+++ b/copyright/copyright_test.go
@@ -2,6 +2,9 @@
// 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 copyright
import (
diff --git a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go
new file mode 100644
index 0000000..1b98a64
--- /dev/null
+++ b/gopls/api-diff/api_diff.go
@@ -0,0 +1,264 @@
+// 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"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ 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) {
+ previousApi, err := loadAPI(prev)
+ if err != nil {
+ return "", err
+ }
+ var currentApi *source.APIJSON
+ if version == "" {
+ currentApi = source.GeneratedAPIJSON
+ } else {
+ var err error
+ currentApi, err = loadAPI(version)
+ if err != nil {
+ return "", 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 "", err
+ }
+ if diff(b, previousApi.Analyzers, currentApi.Analyzers, "analyzer", func(a *source.AnalyzerJSON) string {
+ return a.Name
+ }, diffAnalyzers); err != nil {
+ return "", err
+ }
+ if err := diff(b, previousApi.Lenses, currentApi.Lenses, "code lens", func(l *source.LensJSON) string {
+ return l.Lens
+ }, diffLenses); err != nil {
+ return "", 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 "", 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
+}
+
+func loadAPI(version string) (*source.APIJSON, error) {
+ dir, err := ioutil.TempDir("", "gopath*")
+ if err != nil {
+ return nil, err
+ }
+ defer os.RemoveAll(dir)
+
+ if err := os.Mkdir(fmt.Sprintf("%s/src", dir), 0776); err != nil {
+ return nil, err
+ }
+ goCmd, err := exec.LookPath("go")
+ if err != nil {
+ return nil, err
+ }
+ cmd := exec.Cmd{
+ Path: goCmd,
+ Args: []string{"go", "get", fmt.Sprintf("golang.org/x/tools/gopls@%s", version)},
+ Dir: dir,
+ Env: append(os.Environ(), fmt.Sprintf("GOPATH=%s", dir)),
+ }
+ if err := cmd.Run(); err != nil {
+ return nil, err
+ }
+ cmd = exec.Cmd{
+ Path: filepath.Join(dir, "bin", "gopls"),
+ Args: []string{"gopls", "api-json"},
+ Dir: dir,
+ }
+ out, err := cmd.Output()
+ if err != nil {
+ return nil, err
+ }
+ apiJson := &source.APIJSON{}
+ if err := json.Unmarshal(out, apiJson); err != nil {
+ return nil, 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
+}
diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go
index 91d45ba..b6153e1 100644
--- a/gopls/doc/generate.go
+++ b/gopls/doc/generate.go
@@ -370,7 +370,6 @@
}
func loadCommands(pkg *packages.Package) ([]*source.CommandJSON, error) {
-
var commands []*source.CommandJSON
_, cmds, err := commandmeta.Load()
@@ -553,8 +552,6 @@
return buf.Bytes(), nil
}
-var parBreakRE = regexp.MustCompile("\n{2,}")
-
type optionsGroup struct {
title string
final string
@@ -583,10 +580,8 @@
writeTitle(section, h.final, level)
for _, opt := range h.options {
header := strMultiply("#", level+1)
- fmt.Fprintf(section, "%s **%v** *%v*\n\n", header, opt.Name, opt.Type)
- writeStatus(section, opt.Status)
- enumValues := collectEnums(opt)
- fmt.Fprintf(section, "%v%v\nDefault: `%v`.\n\n", opt.Doc, enumValues, opt.Default)
+ section.Write([]byte(fmt.Sprintf("%s ", header)))
+ opt.Write(section)
}
}
var err error
@@ -657,38 +652,6 @@
return groups
}
-func collectEnums(opt *source.OptionJSON) string {
- var b strings.Builder
- write := func(name, doc string, index, len int) {
- if doc != "" {
- unbroken := parBreakRE.ReplaceAllString(doc, "\\\n")
- fmt.Fprintf(&b, "* %s", unbroken)
- } else {
- fmt.Fprintf(&b, "* `%s`", name)
- }
- if index < len-1 {
- fmt.Fprint(&b, "\n")
- }
- }
- if len(opt.EnumValues) > 0 && opt.Type == "enum" {
- b.WriteString("\nMust be one of:\n\n")
- for i, val := range opt.EnumValues {
- write(val.Value, val.Doc, i, len(opt.EnumValues))
- }
- } else if len(opt.EnumKeys.Keys) > 0 && shouldShowEnumKeysInSettings(opt.Name) {
- b.WriteString("\nCan contain any of:\n\n")
- for i, val := range opt.EnumKeys.Keys {
- write(val.Name, val.Doc, i, len(opt.EnumKeys.Keys))
- }
- }
- return b.String()
-}
-
-func shouldShowEnumKeysInSettings(name string) bool {
- // Both of these fields have too many possible options to print.
- return !hardcodedEnumKeys(name)
-}
-
func hardcodedEnumKeys(name string) bool {
return name == "analyses" || name == "codelenses"
}
@@ -710,20 +673,6 @@
fmt.Fprintf(w, "%s %s\n\n", strMultiply("#", level), capitalize(title))
}
-func writeStatus(section io.Writer, status string) {
- switch status {
- case "":
- case "advanced":
- fmt.Fprint(section, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n")
- case "debug":
- fmt.Fprint(section, "**This setting is for debugging purposes only.**\n\n")
- case "experimental":
- fmt.Fprint(section, "**This setting is experimental and may be deleted.**\n\n")
- default:
- fmt.Fprintf(section, "**Status: %s.**\n\n", status)
- }
-}
-
func capitalize(s string) string {
return string(unicode.ToUpper(rune(s[0]))) + s[1:]
}
@@ -739,13 +688,7 @@
func rewriteCommands(doc []byte, api *source.APIJSON) ([]byte, error) {
section := bytes.NewBuffer(nil)
for _, command := range api.Commands {
- fmt.Fprintf(section, "### **%v**\nIdentifier: `%v`\n\n%v\n\n", command.Title, command.Command, command.Doc)
- if command.ArgDoc != "" {
- fmt.Fprintf(section, "Args:\n\n```\n%s\n```\n\n", command.ArgDoc)
- }
- if command.ResultDoc != "" {
- fmt.Fprintf(section, "Result:\n\n```\n%s\n```\n\n", command.ResultDoc)
- }
+ command.Write(section)
}
return replaceSection(doc, "Commands", section.Bytes())
}
diff --git a/gopls/go.mod b/gopls/go.mod
index f84e502..127b722 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -1,6 +1,6 @@
module golang.org/x/tools/gopls
-go 1.17
+go 1.18
require (
github.com/BurntSushi/toml v0.4.1 // indirect
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 2594cb4..9bc73a9 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -7,6 +7,7 @@
import (
"context"
"fmt"
+ "io"
"path/filepath"
"regexp"
"strings"
@@ -1280,6 +1281,69 @@
Hierarchy string
}
+func (o *OptionJSON) String() string {
+ return o.Name
+}
+
+func (o *OptionJSON) Write(w io.Writer) {
+ fmt.Fprintf(w, "**%v** *%v*\n\n", o.Name, o.Type)
+ writeStatus(w, o.Status)
+ enumValues := collectEnums(o)
+ fmt.Fprintf(w, "%v%v\nDefault: `%v`.\n\n", o.Doc, enumValues, o.Default)
+}
+
+func writeStatus(section io.Writer, status string) {
+ switch status {
+ case "":
+ case "advanced":
+ fmt.Fprint(section, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n")
+ case "debug":
+ fmt.Fprint(section, "**This setting is for debugging purposes only.**\n\n")
+ case "experimental":
+ fmt.Fprint(section, "**This setting is experimental and may be deleted.**\n\n")
+ default:
+ fmt.Fprintf(section, "**Status: %s.**\n\n", status)
+ }
+}
+
+var parBreakRE = regexp.MustCompile("\n{2,}")
+
+func collectEnums(opt *OptionJSON) string {
+ var b strings.Builder
+ write := func(name, doc string, index, len int) {
+ if doc != "" {
+ unbroken := parBreakRE.ReplaceAllString(doc, "\\\n")
+ fmt.Fprintf(&b, "* %s", unbroken)
+ } else {
+ fmt.Fprintf(&b, "* `%s`", name)
+ }
+ if index < len-1 {
+ fmt.Fprint(&b, "\n")
+ }
+ }
+ if len(opt.EnumValues) > 0 && opt.Type == "enum" {
+ b.WriteString("\nMust be one of:\n\n")
+ for i, val := range opt.EnumValues {
+ write(val.Value, val.Doc, i, len(opt.EnumValues))
+ }
+ } else if len(opt.EnumKeys.Keys) > 0 && shouldShowEnumKeysInSettings(opt.Name) {
+ b.WriteString("\nCan contain any of:\n\n")
+ for i, val := range opt.EnumKeys.Keys {
+ write(val.Name, val.Doc, i, len(opt.EnumKeys.Keys))
+ }
+ }
+ return b.String()
+}
+
+func shouldShowEnumKeysInSettings(name string) bool {
+ // Both of these fields have too many possible options to print.
+ return !hardcodedEnumKeys(name)
+}
+
+func hardcodedEnumKeys(name string) bool {
+ return name == "analyses" || name == "codelenses"
+}
+
type EnumKeys struct {
ValueType string
Keys []EnumKey
@@ -1304,14 +1368,44 @@
ResultDoc string
}
+func (c *CommandJSON) String() string {
+ return c.Command
+}
+
+func (c *CommandJSON) Write(w io.Writer) {
+ fmt.Fprintf(w, "### **%v**\nIdentifier: `%v`\n\n%v\n\n", c.Title, c.Command, c.Doc)
+ if c.ArgDoc != "" {
+ fmt.Fprintf(w, "Args:\n\n```\n%s\n```\n\n", c.ArgDoc)
+ }
+ if c.ResultDoc != "" {
+ fmt.Fprintf(w, "Result:\n\n```\n%s\n```\n\n", c.ResultDoc)
+ }
+}
+
type LensJSON struct {
Lens string
Title string
Doc string
}
+func (l *LensJSON) String() string {
+ return l.Title
+}
+
+func (l *LensJSON) Write(w io.Writer) {
+ fmt.Fprintf(w, "%s (%s): %s", l.Title, l.Lens, l.Doc)
+}
+
type AnalyzerJSON struct {
Name string
Doc string
Default bool
}
+
+func (a *AnalyzerJSON) String() string {
+ return a.Name
+}
+
+func (a *AnalyzerJSON) Write(w io.Writer) {
+ fmt.Fprintf(w, "%s (%s): %v", a.Name, a.Doc, a.Default)
+}