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) +}