tools: run gopls setting update in tools/generate.go

Made `tools/goplssetting` to be a library package
so the functionality can be called from tools/generate.go.
Since this is in a separate package, calling this is tricky
without being in the same module. Create go.mod.

I initially wanted to have go.mod under tools directory
but that breaks `go run tools/generate.go` from
the project root directory.

Simplified the jq query - the original one was written
when we attempted to define all gopls settings in the
top-level. We changed to define them inside "gopls"
object, it can be simpler. Also, it seems like jq
in the distro is outdated and doesn't support the 'walk'
function.

Removed 'unusedwrite' from the available analysis
list in the settings.md doc. It's not available in 0.6.6,
but added by mistake. Now this is included in CI and
CI currently uses the 'latest' for generation, this
extra setting not available in the current latest
will cause failure.

Fixes golang/vscode-go#1280

Change-Id: Ia1b153d079dfec932b8ddbe946e7352dc405a473
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/299949
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/build/Dockerfile b/build/Dockerfile
index 0deeb14..17662f0 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -26,7 +26,7 @@
 ENV PATH /workspace/vscodego/go/bin:/go/bin:/usr/local/go/bin:${PATH}
 ENV DEBIAN_FRONTEND noninteractive
 
-RUN apt-get -qq update && apt-get install -qq -y libnss3 libgtk-3-dev libxss1 libasound2 xvfb libsecret-1-0 > /dev/null
+RUN apt-get -qq update && apt-get install -qq -y libnss3 libgtk-3-dev libxss1 libasound2 xvfb libsecret-1-0 jq > /dev/null
 RUN npm install -g typescript vsce
 
 WORKDIR /workspace
diff --git a/build/all.bash b/build/all.bash
index fa80da7..0f6a338 100755
--- a/build/all.bash
+++ b/build/all.bash
@@ -51,7 +51,7 @@
   npm run lint
 
   echo "**** Run settings generator ****"
-  go run tools/generate.go -w=false
+  go run tools/generate.go -w=false -gopls=true
 
   echo "**** Check if vsce works ****"
   vsce package
diff --git a/docs/settings.md b/docs/settings.md
index 36ef931..79494d2 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -672,7 +672,6 @@
 | `unsafeptr` | check for invalid conversions of uintptr to unsafe.Pointer <br/> The unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer to convert integers to pointers. A conversion from uintptr to unsafe.Pointer is invalid if it implies that there is a uintptr-typed word in memory that holds a pointer value, because that word will be invisible to stack copying and to the garbage collector. <br/> Default: `true` |
 | `unusedparams` | check for unused parameters of functions <br/> The unusedparams analyzer checks functions to see if there are any parameters that are not being used. <br/> To reduce false positives it ignores: - methods - parameters that do not have a name or are underscored - functions in test files - functions with empty bodies or those with just a return stmt <br/> Default: `false` |
 | `unusedresult` | check for unused results of calls to some functions <br/> Some functions like fmt.Errorf return a result and have no side effects, so it is always a mistake to discard the result. This analyzer reports calls to certain functions in which the result of the call is ignored. <br/> The set of functions may be controlled using flags. <br/> Default: `true` |
-| `unusedwrite` | checks for unused writes <br/> The analyzer reports instances of writes to struct fields and arrays that are never read. Specifically, when a struct object or an array is copied, its elements are copied implicitly by the compiler, and any element write to this copy does nothing with the original object. <br/> For example: <br/> <pre>type T struct { x int }<br/>func f(input []T) {<br/>	for i, v := range input {  // v is a copy<br/>		v.x = i  // unused write to field x<br/>	}<br/>}</pre><br/> Another example is about non-pointer receiver: <br/> <pre>type T struct { x int }<br/>func (t T) f() {  // t is a copy<br/>	t.x = i  // unused write to field x<br/>}</pre><br/> <br/> Default: `false` |
 ### `ui.diagnostic.annotations`
 
 (Experimental) annotations specifies the various kinds of optimization diagnostics
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c9b6ca1
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/golang/vscode-go
+
+go 1.16
diff --git a/package.json b/package.json
index 393fb6c..df94795 100644
--- a/package.json
+++ b/package.json
@@ -2113,11 +2113,6 @@
                   "type": "boolean",
                   "markdownDescription": "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side effects,\nso it is always a mistake to discard the result. This analyzer reports\ncalls to certain functions in which the result of the call is ignored.\n\nThe set of functions may be controlled using flags.",
                   "default": true
-                },
-                "unusedwrite": {
-                  "type": "boolean",
-                  "markdownDescription": "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\tfunc f(input []T) {\n\t\tfor i, v := range input {  // v is a copy\n\t\t\tv.x = i  // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\tfunc (t T) f() {  // t is a copy\n\t\tt.x = i  // unused write to field x\n\t}\n",
-                  "default": false
                 }
               }
             },
diff --git a/tools/generate.go b/tools/generate.go
index acaada5..9999738 100644
--- a/tools/generate.go
+++ b/tools/generate.go
@@ -2,9 +2,14 @@
 // Licensed under the MIT License.
 // See LICENSE in the project root for license information.
 
-// Command generate is used to generate documentation from the package.json.
-// To run:
-// go run tools/generate.go -w
+// 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 (
@@ -18,10 +23,15 @@
 	"path/filepath"
 	"sort"
 	"strings"
+
+	"github.com/golang/vscode-go/tools/goplssetting"
 )
 
 var (
-	writeFlag = flag.Bool("w", true, "Write new file contents to disk.")
+	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.")
+
+	debugFlag = flag.Bool("debug", false, "If true, enable extra logging and skip deletion of intermediate files.")
 )
 
 type PackageJSON struct {
@@ -63,11 +73,23 @@
 	if err != nil {
 		log.Fatal(err)
 	}
+
+	packageJSONFile := filepath.Join(dir, "package.json")
+
 	// Find the package.json file.
-	data, err := ioutil.ReadFile(filepath.Join(dir, "package.json"))
+	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)
@@ -444,3 +466,24 @@
 	}
 	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
+}
diff --git a/tools/goplssetting/main.go b/tools/goplssetting/goplssetting.go
similarity index 87%
rename from tools/goplssetting/main.go
rename to tools/goplssetting/goplssetting.go
index fc465ba..3c9f331 100644
--- a/tools/goplssetting/main.go
+++ b/tools/goplssetting/goplssetting.go
@@ -2,16 +2,11 @@
 // Licensed under the MIT License.
 // See LICENSE in the project root for license information.
 
-// This command updates the gopls.* configurations in vscode-go package.json.
-//
-//   Usage: from the project root directory,
-//      $ go run tools/goplssetting/main.go -in ./package.json -out ./package.json
-package main
+package goplssetting
 
 import (
 	"bytes"
 	"encoding/json"
-	"flag"
 	"fmt"
 	"io/ioutil"
 	"log"
@@ -22,45 +17,25 @@
 	"strings"
 )
 
-var (
-	inPkgJSON  = flag.String("in", "", "input package.json location")
-	outPkgJSON = flag.String("out", "", "output package.json location. If empty, output to the standard output.")
-
-	work = flag.Bool("w", false, "if true, do not delete intermediate files")
-)
-
-func main() {
-	flag.Parse()
-
-	if *inPkgJSON == "" {
-		log.Fatalf("-in file must be specified %q %q", *inPkgJSON, *outPkgJSON)
-	}
-	if _, err := os.Stat(*inPkgJSON); err != nil {
-		log.Fatalf("failed to find input package.json (%q): %v", *inPkgJSON, err)
+// Generate reads package.json and updates the gopls settings section
+// based on `gopls api-json` output. This function requires `jq` to
+// manipulate package.json.
+func Generate(inputFile string, skipCleanup bool) ([]byte, error) {
+	if _, err := os.Stat(inputFile); err != nil {
+		return nil, err
 	}
 
-	out, err := run(*inPkgJSON)
-	if err != nil {
-		log.Fatal(err)
+	if _, err := exec.LookPath("jq"); err != nil {
+		return nil, fmt.Errorf("missing `jq`: %w", err)
 	}
-	if *outPkgJSON != "" {
-		if err := ioutil.WriteFile(*outPkgJSON, out, 0644); err != nil {
-			log.Fatalf("writing jq output to %q failed: %v", out, err)
-		}
-	} else {
-		fmt.Printf("%s", out)
-	}
-}
 
-// run
-func run(orgPkgJSON string) ([]byte, error) {
 	workDir, err := ioutil.TempDir("", "goplssettings")
 	if err != nil {
 		return nil, err
 	}
 	log.Printf("WORK=%v", workDir)
 
-	if !*work {
+	if !skipCleanup {
 		defer os.RemoveAll(workDir)
 	}
 
@@ -87,7 +62,7 @@
 		return nil, err
 	}
 
-	return rewritePackageJSON(f.Name(), orgPkgJSON)
+	return rewritePackageJSON(f.Name(), inputFile)
 }
 
 // readGoplsAPI returns the output of `gopls api-json`.
@@ -183,8 +158,7 @@
 // to update all existing gopls settings with the ones from the newSettings
 // file.
 func rewritePackageJSON(newSettings, inFile string) ([]byte, error) {
-	prog := `walk(if type == "object" then with_entries(select(.key | test("^gopls.[a-z]") | not)) else . end) | .contributes.configuration.properties *= $GOPLS_SETTINGS[0]`
-
+	prog := `.contributes.configuration.properties+=$GOPLS_SETTINGS[0]`
 	cmd := exec.Command("jq", "--slurpfile", "GOPLS_SETTINGS", newSettings, prog, inFile)
 	var stdout, stderr bytes.Buffer
 	cmd.Stdout = &stdout
diff --git a/tools/goplssetting/main_test.go b/tools/goplssetting/goplssetting_test.go
similarity index 97%
rename from tools/goplssetting/main_test.go
rename to tools/goplssetting/goplssetting_test.go
index 7147347..80d553f 100644
--- a/tools/goplssetting/main_test.go
+++ b/tools/goplssetting/goplssetting_test.go
@@ -2,7 +2,7 @@
 // Licensed under the MIT License.
 // See LICENSE in the project root for license information.
 
-package main
+package goplssetting
 
 import (
 	"bytes"
@@ -20,7 +20,7 @@
 		t.Skipf("jq is not found (%v), skipping...", err)
 	}
 	testfile := filepath.Join("..", "..", "package.json")
-	got, err := run(testfile)
+	got, err := Generate(testfile, false)
 	if err != nil {
 		t.Fatalf("run failed: %v", err)
 	}