blob: e3169d18e01e9f72c65838536df2b1a99eba7ea3 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Licensed under the MIT License.
// See LICENSE in the project root for license information.
// Command generate is used to update package.json based on
// the gopls's API and generate documentation from it.
//
// To update documentation based on the current package.json:
// go run tools/generate.go
//
// To update package.json and generate documentation.
// go run tools/generate.go -gopls
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/golang/vscode-go/tools/goplssetting"
)
var (
writeFlag = flag.Bool("w", true, "Write new file contents to disk.")
updateGoplsSettingsFlag = flag.Bool("gopls", false, "Update gopls settings in package.json. This is disabled by default because 'jq' tool is needed for generation.")
updateLatestToolVersionsFlag = flag.Bool("tools", false, "Update the latest versions of tools in src/src/goToolsInformation.ts. This is disabled by default because the latest versions may change frequently and should not block a release.")
debugFlag = flag.Bool("debug", false, "If true, enable extra logging and skip deletion of intermediate files.")
)
func checkAndWrite(filename string, oldContent, newContent []byte) {
// Return early if the contents are unchanged.
if bytes.Equal(oldContent, newContent) {
return
}
// Either write out new contents or report an error (if in CI).
if *writeFlag {
if err := ioutil.WriteFile(filename, newContent, 0644); err != nil {
log.Fatal(err)
}
fmt.Printf("updated %s\n", filename)
} else {
base := filepath.Join("docs", filepath.Base(filename))
fmt.Printf(`%s have changed in the package.json, but documentation in %s was not updated.
To update the settings, run "go run tools/generate.go -w".
`, strings.TrimSuffix(base, ".md"), base)
os.Exit(1) // causes CI to break.
}
}
type PackageJSON struct {
Contributes struct {
Commands []Command `json:"commands,omitempty"`
Configuration struct {
Properties map[string]*Property `json:"properties,omitempty"`
} `json:"configuration,omitempty"`
Debuggers []Debugger `json:"debuggers,omitempty"`
} `json:"contributes,omitempty"`
}
type Command struct {
Command string `json:"command,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
}
type Property struct {
name string `json:"name,omitempty"` // Set by us.
// Below are defined in package.json
Properties map[string]*Property `json:"properties,omitempty"`
AnyOf []Property `json:"anyOf,omitempty"`
Default interface{} `json:"default,omitempty"`
MarkdownDescription string `json:"markdownDescription,omitempty"`
Description string `json:"description,omitempty"`
MarkdownDeprecationMessage string `json:"markdownDeprecationMessage,omitempty"`
DeprecationMessage string `json:"deprecationMessage,omitempty"`
Type interface{} `json:"type,omitempty"`
Enum []interface{} `json:"enum,omitempty"`
EnumDescriptions []string `json:"enumDescriptions,omitempty"`
MarkdownEnumDescriptions []string `json:"markdownEnumDescriptions,omitempty"`
Items *Property `json:"items,omitempty"`
}
type Debugger struct {
Type string `json:"type,omitempty"`
Label string `json:"label,omitempty"`
ConfigurationAttributes struct {
Launch Configuration
Attach Configuration
} `json:"configurationAttributes,omitempty"`
}
type Configuration struct {
Properties map[string]*Property `json:"properties,omitempty"`
}
type moduleVersion struct {
Path string `json:",omitempty"`
Version string `json:",omitempty"`
Time string `json:",omitempty"`
Versions []string `json:",omitempty"`
}
func main() {
flag.Parse()
// Assume this is running from the vscode-go directory.
dir, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
packageJSONFile := filepath.Join(dir, "package.json")
// Find the package.json file.
data, err := ioutil.ReadFile(packageJSONFile)
if err != nil {
log.Fatal(err)
}
if *updateGoplsSettingsFlag {
newData, err := updateGoplsSettings(data, packageJSONFile, *debugFlag)
if err != nil {
log.Fatal(err)
}
data = newData
}
pkgJSON := &PackageJSON{}
if err := json.Unmarshal(data, pkgJSON); err != nil {
log.Fatal(err)
}
rewrite := func(filename string, toAdd []byte) {
oldContent, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
gen := []byte(`<!-- Everything below this line is generated. DO NOT EDIT. -->`)
split := bytes.Split(oldContent, gen)
if len(split) == 1 {
log.Fatalf("expected to find %q in %s, not found", gen, filename)
}
var s []byte
if strings.HasSuffix(filename, ".ts") {
s = bytes.Join([][]byte{
split[0],
gen,
[]byte("\n\n"),
toAdd,
}, []byte{})
} else {
s = bytes.Join([][]byte{
bytes.TrimSpace(split[0]),
gen,
toAdd,
}, []byte("\n\n"))
}
newContent := append(s, '\n')
checkAndWrite(filename, oldContent, newContent)
}
b := &bytes.Buffer{}
for i, c := range pkgJSON.Contributes.Commands {
fmt.Fprintf(b, "### `%s`\n\n%s", c.Title, c.Description)
if i != len(pkgJSON.Contributes.Commands)-1 {
b.WriteString("\n\n")
}
}
rewrite(filepath.Join(dir, "docs", "commands.md"), b.Bytes())
// Clear so that we can rewrite settings.md.
b.Reset()
var properties []*Property
var goplsProperty *Property
for name, p := range pkgJSON.Contributes.Configuration.Properties {
p.name = name
if name == "gopls" {
goplsProperty = p
}
properties = append(properties, p)
}
sort.Slice(properties, func(i, j int) bool {
return properties[i].name < properties[j].name
})
for _, p := range properties {
if p.name == "gopls" {
desc := "Customize `gopls` behavior by specifying the gopls' settings in this section. " +
"For example, \n```\n\"gopls\" : {\n\t\"build.directoryFilters\": [\"-node_modules\"]\n\t...\n}\n```\n" +
"This section is directly read by `gopls`. See the [`gopls` section](#settings-for-gopls) section " +
"for the full list of `gopls` settings."
fmt.Fprintf(b, "### `%s`\n\n%s", p.name, desc)
b.WriteString("\n\n")
continue
}
writeProperty(b, "###", p)
b.WriteString("\n")
}
// Write gopls section.
b.WriteString("## Settings for `gopls`\n\n")
writeGoplsSettingsSection(b, goplsProperty)
rewrite(filepath.Join(dir, "docs", "settings.md"), b.Bytes())
b.Reset()
generateDebugConfigTable(b, pkgJSON)
rewriteDebugDoc(filepath.Join(dir, "docs", "debugging.md"), b.Bytes())
// Only update the latest tool versions if the flag is set.
if !*updateLatestToolVersionsFlag {
return
}
// Clear so that we can rewrite src/goToolsInformation.ts.
b.Reset()
// Check for latest dlv-dap version.
dlvVersion, err := listModuleVersion("github.com/go-delve/delve@master")
if err != nil {
log.Fatal(err)
}
// Due to https://github.com/golang/vscode-go/issues/1682, we cannot use
// pseudo-version as the pinned version reliably.
dlvRevOrStable := dlvVersion.Version
if rev, err := pseudoVersionRev(dlvVersion.Version); err == nil { // pseudo-version
dlvRevOrStable = rev
}
// Check for the latest gopls version.
versions, err := listAllModuleVersions("golang.org/x/tools/gopls")
if err != nil {
log.Fatal(err)
}
latestIndex := len(versions.Versions) - 1
latestPre := versions.Versions[latestIndex]
// We need to find the last version that was not a pre-release.
var latest string
for latest = versions.Versions[latestIndex]; latestIndex >= 0; latestIndex-- {
if !strings.Contains(latest, "pre") {
break
}
}
goplsVersion, err := listModuleVersion(fmt.Sprintf("golang.org/x/tools/gopls@%s", latest))
if err != nil {
log.Fatal(err)
}
goplsVersionPre, err := listModuleVersion(fmt.Sprintf("golang.org/x/tools/gopls@%s", latestPre))
if err != nil {
log.Fatal(err)
}
allToolsFile := filepath.Join(dir, "tools", "allTools.ts.in")
// Find the package.json file.
data, err = ioutil.ReadFile(allToolsFile)
if err != nil {
log.Fatal(err)
}
// TODO(suzmue): change input to json and avoid magic string printing.
toolsString := fmt.Sprintf(string(data), goplsVersion.Version, goplsVersion.Time[:len("YYYY-MM-DD")], goplsVersionPre.Version, goplsVersionPre.Time[:len("YYYY-MM-DD")], dlvRevOrStable, dlvVersion.Version, dlvVersion.Time[:len("YYYY-MM-DD")])
// Write tools section.
b.WriteString(toolsString)
rewrite(filepath.Join(dir, "src", "goToolsInformation.ts"), b.Bytes())
}
func listModuleVersion(path string) (moduleVersion, error) {
output, err := exec.Command("go", "list", "-m", "-json", path).Output()
if err != nil {
return moduleVersion{}, err
}
var version moduleVersion
err = json.Unmarshal(output, &version)
if err != nil {
return moduleVersion{}, err
}
return version, nil
}
func listAllModuleVersions(path string) (moduleVersion, error) {
output, err := exec.Command("go", "list", "-m", "-json", "-versions", path).Output()
if err != nil {
return moduleVersion{}, err
}
var version moduleVersion
err = json.Unmarshal(output, &version)
if err != nil {
return moduleVersion{}, err
}
return version, nil
}
func writeProperty(b *bytes.Buffer, heading string, p *Property) {
desc := p.Description
if p.MarkdownDescription != "" {
desc = p.MarkdownDescription
}
deprecation := p.DeprecationMessage
if p.MarkdownDeprecationMessage != "" {
deprecation = p.MarkdownDeprecationMessage
}
name := p.name
if deprecation != "" {
name += " (deprecated)"
desc = deprecation + "\n" + desc
}
fmt.Fprintf(b, "%s `%s`\n\n%s", heading, name, desc)
if enums := enumDescriptionsSnippet(p); enums != "" {
fmt.Fprintf(b, "<br/>\n%s", enums)
}
if p.Type == "object" {
writeSettingsObjectProperties(b, p.Properties)
}
if defaults := defaultDescriptionSnippet(p); defaults != "" {
b.WriteString("\n\n")
if p.Type == "object" {
fmt.Fprintf(b, "Default:\n```\n%v\n```", defaults)
} else {
fmt.Fprintf(b, "Default: `%v`", defaults)
}
}
}
func defaultDescriptionSnippet(p *Property) string {
if p.Default == nil {
return ""
}
b := &bytes.Buffer{}
switch p.Type {
case "object":
x, ok := p.Default.(map[string]interface{})
// do nothing if it is nil
if ok && len(x) > 0 {
writeMapObject(b, "", x)
}
case "string":
fmt.Fprintf(b, "%q", p.Default)
case "boolean", "number":
fmt.Fprintf(b, "%v", p.Default)
case "array":
if x, ok := p.Default.([]interface{}); ok && len(x) > 0 {
fmt.Fprintf(b, "%v", p.Default)
}
default:
fmt.Fprintf(b, "%v", p.Default)
}
return b.String()
}
func writeMapObject(b *bytes.Buffer, indent string, obj map[string]interface{}) {
keys := []string{}
for k := range obj {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Fprintf(b, "%v{\n", indent)
for _, k := range keys {
fmt.Fprintf(b, "%v%q :\t", indent+"\t", k)
v := obj[k]
switch v := v.(type) {
case string:
fmt.Fprintf(b, "%q", v)
case map[string]interface{}:
writeMapObject(b, indent+"\t", v)
default:
fmt.Fprintf(b, "%v", v)
}
fmt.Fprint(b, ",\n")
}
fmt.Fprintf(b, "%v}", indent)
}
func writeGoplsSettingsSection(b *bytes.Buffer, goplsProperty *Property) {
desc := goplsProperty.MarkdownDescription
b.WriteString(desc)
b.WriteString("\n\n")
properties := goplsProperty.Properties
var names []string
for name := range properties {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
p := properties[name]
p.name = name
writeProperty(b, "###", p)
b.WriteString("\n")
}
}
func writeSettingsObjectProperties(b *bytes.Buffer, properties map[string]*Property) {
if len(properties) == 0 {
return
}
var names []string
for name := range properties {
names = append(names, name)
}
sort.Strings(names)
b.WriteString("\n")
b.WriteString("| Properties | Description |\n")
b.WriteString("| --- | --- |\n")
ending := "\n"
for i, name := range names {
if i == len(names)-1 {
ending = ""
}
p := properties[name]
p.name = name
desc := p.Description
if p.MarkdownDescription != "" {
desc = p.MarkdownDescription
}
deprecation := p.DeprecationMessage
if p.MarkdownDeprecationMessage != "" {
deprecation = p.MarkdownDeprecationMessage
}
if deprecation != "" {
name += " (deprecated)"
desc = deprecation + "\n" + desc
}
if enum := enumDescriptionsSnippet(p); enum != "" {
desc += "\n\n" + enum
}
if defaults := defaultDescriptionSnippet(p); defaults != "" {
desc += "\n\n"
if p.Type == "object" {
desc += fmt.Sprintf("Default:\n```\n%v\n```", defaults)
} else {
desc += fmt.Sprintf("Default: `%v`", defaults)
}
}
desc = gocommentToMarkdown(desc)
fmt.Fprintf(b, "| `%s` | %s |%v", name, desc, ending)
}
}
// enumDescriptionsSnippet returns the snippet for the allowed values.
func enumDescriptionsSnippet(p *Property) string {
b := &bytes.Buffer{}
if len(p.Enum) == 0 {
return ""
}
desc := p.EnumDescriptions
if len(p.MarkdownEnumDescriptions) != 0 {
desc = p.MarkdownEnumDescriptions
}
hasDesc := false
for _, d := range desc {
if d != "" {
hasDesc = true
break
}
}
b.WriteString("Allowed Options:")
if hasDesc && len(desc) == len(p.Enum) {
b.WriteString("\n\n")
for i, e := range p.Enum {
fmt.Fprintf(b, "* `%v`", e)
if d := desc[i]; d != "" {
fmt.Fprintf(b, ": %v", strings.TrimRight(strings.ReplaceAll(d, "\n\n", "<br/>"), "\n"))
}
b.WriteString("\n")
}
} else {
for i, e := range p.Enum {
fmt.Fprintf(b, " `%v`", e)
if i < len(p.Enum)-1 {
b.WriteString(",")
}
}
}
return b.String()
}
// gocommentToMarkdown converts the description string generated based on go comments
// to more markdown-friendly style.
// - treat indented lines as pre-formatted blocks (e.g. code snippets) like in go doc
// - replace new lines with <br/>'s, so the new lines mess up formatting when embedded in tables
// - preserve new lines inside preformatted sections, but replace them with <br/>'s
// - skip unneeded new lines
func gocommentToMarkdown(s string) string {
lines := strings.Split(s, "\n")
inPre := false
b := &bytes.Buffer{}
for i, l := range lines {
if strings.HasPrefix(l, "\t") { // indented
if !inPre { // beginning of the block
inPre = true
b.WriteString("<pre>")
} else { // preserve new lines in pre-formatted block
b.WriteString("<br/>")
}
l = l[1:] // remove one leading \t, in favor of <pre></pre> formatting.
} else { // not indented
if inPre {
inPre = false
b.WriteString("</pre>")
}
}
if l == "" && i != len(lines)-1 {
b.WriteString("<br/>") // add a new line.
} else {
b.WriteString(l) // just print l, no new line.
}
if i != len(lines)-1 {
if !inPre {
b.WriteString(" ")
}
}
}
return b.String()
}
func updateGoplsSettings(oldData []byte, packageJSONFile string, debug bool) (newData []byte, _ error) {
newData, err := goplssetting.Generate(packageJSONFile, debug)
if err != nil { // failed to compute up-to-date gopls settings.
return nil, err
}
if bytes.Equal(oldData, newData) {
return oldData, nil
}
if !*writeFlag {
fmt.Println(`gopls settings section in package.json needs update. To update the settings, run "go run tools/generate.go -w -gopls".`)
os.Exit(1) // causes CI to break.
}
if err := ioutil.WriteFile(packageJSONFile, newData, 0644); err != nil {
return nil, err
}
return newData, nil
}
func rewriteDebugDoc(filename string, toAdd []byte) {
oldContent, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
startSep := []byte(`<!-- SETTINGS BEGIN -->`)
endSep := []byte(`<!-- SETTINGS END -->`)
startIdx := bytes.Index(oldContent, startSep)
endIdx := bytes.Index(oldContent, endSep)
if startIdx <= 0 || endIdx <= startIdx {
log.Fatalf("Missing valid SETTINGS BEGIN/END markers in %v", filename)
}
part1 := oldContent[:startIdx+len(startSep)+1]
part3 := oldContent[endIdx:]
newContent := bytes.Join([][]byte{
part1,
toAdd,
part3,
}, []byte{})
checkAndWrite(filename, oldContent, newContent)
}
func generateDebugConfigTable(w io.Writer, pkgJSON *PackageJSON) {
for _, d := range pkgJSON.Contributes.Debuggers {
table := map[string]bool{}
for k := range d.ConfigurationAttributes.Attach.Properties {
table[k] = true
}
for k := range d.ConfigurationAttributes.Launch.Properties {
table[k] = true
}
keys := make([]string, 0, len(table))
for k := range table {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Fprintln(w, "| Property | Launch | Attach |")
fmt.Fprintln(w, "| --- | --- | --- |")
for _, k := range keys {
launch := describeDebugProperty(d.ConfigurationAttributes.Launch.Properties[k])
attach := describeDebugProperty(d.ConfigurationAttributes.Attach.Properties[k])
if launch != "" && attach != "" {
if launch != attach {
fmt.Fprintf(w, "| `%v` | %v | %v |\n", k, launch, attach)
} else {
fmt.Fprintf(w, "| `%v` | %v | <center>_same as Launch_</center>|\n", k, launch)
}
} else if launch != "" {
fmt.Fprintf(w, "| `%v` | %v | <center>_n/a_</center> |\n", k, launch)
} else if attach != "" {
fmt.Fprintf(w, "| `%v` | <center>_n/a_</center> | %v |\n", k, attach)
}
}
}
}
func describeDebugProperty(p *Property) string {
if p == nil {
return ""
}
b := &bytes.Buffer{}
desc := p.Description
if p.MarkdownDescription != "" {
desc = p.MarkdownDescription
}
if p == nil || strings.Contains(desc, "Not applicable when using `dlv-dap` mode.") {
return ""
}
deprecation := p.DeprecationMessage
if p.MarkdownDeprecationMessage != "" {
deprecation = p.MarkdownDeprecationMessage
}
if deprecation != "" {
fmt.Fprintf(b, "(Deprecated) *%v*<br/>", deprecation)
}
fmt.Fprintf(b, "%v<br/>", desc)
if len(p.AnyOf) > 0 {
for i, a := range p.AnyOf {
fmt.Fprintf(b, "<p><b>Option %d:</b> %v<br/>", i+1, describeDebugProperty(&a))
}
}
if len(p.Enum) > 0 {
var enums []string
for _, i := range p.Enum {
enums = append(enums, fmt.Sprintf("`%#v`", i))
}
fmt.Fprintf(b, "<p>Allowed Values: %v<br/>", strings.Join(enums, ", "))
}
if p.Type == "object" && len(p.Properties) > 0 {
var keys []string
for k := range p.Properties {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Fprintf(b, "<ul>")
for _, k := range keys {
v := p.Properties[k]
fmt.Fprintf(b, "<li>`%q`: %v</li>", k, describeDebugProperty(v))
}
fmt.Fprintf(b, "</ul>")
}
if p.Type == "array" && p.Items != nil && p.Items.Type == "object" {
fmt.Fprintf(b, "<p>%v<br/>", describeDebugProperty(p.Items))
}
// Default
if d := defaultDescriptionSnippet(p); d != "" {
fmt.Fprintf(b, "(Default: `%v`)<br/>", d)
}
return b.String()
}
// pseudoVersionRev extracts the revision info if the given version is pseudo version.
// We wanted to use golang.org/x/mod/module.PseudoVersionRev, but couldn't due to
// an error in the CI. This is a workaround.
//
// It assumes the version string came from the proxy, so a valid, canonical version
// string. Thus, the check for pseudoversion is not as robust as golang.org/x/mod/module
// offers.
func pseudoVersionRev(ver string) (rev string, _ error) {
var pseudoVersionRE = regexp.MustCompile(`^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`)
if strings.Count(ver, "-") < 2 || !pseudoVersionRE.MatchString(ver) {
return "", errors.New("not a pseudo version")
}
j := strings.LastIndex(ver, "-")
return ver[j+1:], nil
}