[release] prepare v0.37.0 release

7df32c8f CHANGELOG.md: addresses comments from cl/457476
f820eaef CHANGELOG.md: change log for v0.37.0
107463e2 package-lock.json: npm audit fix
2f29f891 src/welcome: vulncheck analyzer announcement for v0.37.0
d28aeac9 src/goTest: add codelens for sub tests
357391a3 docs/features: add vulncheck feature description
a64573f3 src/goVulncheck: add the feedback link, polish govulncheck output
6c62e5d1 src/goVulncheck: add go.vulncheck.toggle command
087370a3 goplsSurvey: remove lumpiness in survey days
c141ad80 package.json: add 'go.diagnostic.vulncheck'
7848fb37 src/goVulncheck: add vulncheck output links provider
b2decd20 src/goVulncheck2: rename goVulncheck2.ts to goVulncheck.ts
c40f0734 src/goVulncheck: remove the experimental "Go: Run Vulncheck" command
8ffb271d syntaxes: color govulncheck output in the output channel
3bcbaafd src/goVulncheck2: output govulncheck progress and result
aff24c55 test/unit/goDebug: delete the goDebug test
35966351 package.json: sync gopls settings (gopls@v0.11.0-pre.1)
34c0efbd src/goDebugFactory: re-enable version check
800c2331 src/goVulncheck: update gopls govulncheck command name
f76aad62 src/context: add govulncheckOutputChannel
3a4f19df src/debugAdapter: fix typo in panic message
5be77168 .github/workflows: update node version to 18
e51a4d72 src/goEnvironment: make the window reload request visible
1da1ea2c package-lock.json: bump loader-utils from 1.4.0 to 1.4.1
28f7c073 src/goInstallTools: handle unknown tools
8f81613e docs/settings: make the custom formatter support more visible
752577ea test/gopls: narrow the scope of fake formatTool setting
b713787d docs/features: mention inlay hints and update diagnostics part
b54d132c .github/workflows/wiki: use cp instead of diff to handle binaries
f67d75f6 src/goCover: ignore bogus-looking line/column data
91fe72d9 package.json: start of v0.37.0 dev
12dabf30 tools/license.sh: add CC-BY-4.0 license

Change-Id: I89299b79b7939a759f875073baeedad6c6b8e346
diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index 1da361c..682cb7c 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -23,7 +23,7 @@
       - name: Setup Node
         uses: actions/setup-node@v3
         with:
-         node-version: '16'
+         node-version: '18'
          cache: 'npm'
 
       - name: Setup Go
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 46bba60..342a43f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -43,7 +43,7 @@
       - name: Setup Node
         uses: actions/setup-node@v3
         with:
-          node-version: '16'
+          node-version: '18'
           cache: 'npm'
 
       - name: get release version
diff --git a/.github/workflows/test-long-all.yml b/.github/workflows/test-long-all.yml
index 0a39a63..2b700b9 100644
--- a/.github/workflows/test-long-all.yml
+++ b/.github/workflows/test-long-all.yml
@@ -26,7 +26,7 @@
       - name: Setup Node
         uses: actions/setup-node@v3
         with:
-         node-version: '16'
+         node-version: '18'
          cache: 'npm'
 
       - name: Setup Go
diff --git a/.github/workflows/test-long.yml b/.github/workflows/test-long.yml
index 79b783d..634ac58 100644
--- a/.github/workflows/test-long.yml
+++ b/.github/workflows/test-long.yml
@@ -25,7 +25,7 @@
       - name: Setup Node
         uses: actions/setup-node@v3
         with:
-         node-version: '16'
+         node-version: '18'
          cache: 'npm'
 
       - name: Setup Go
diff --git a/.github/workflows/test-smoke.yml b/.github/workflows/test-smoke.yml
index 2c24d20..46bd23e 100644
--- a/.github/workflows/test-smoke.yml
+++ b/.github/workflows/test-smoke.yml
@@ -24,7 +24,7 @@
       - name: Setup Node
         uses: actions/setup-node@v3
         with:
-         node-version: '16'
+         node-version: '18'
          cache: 'npm'
 
       - name: Setup Go
diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml
index 45ed20d..c55129d 100644
--- a/.github/workflows/wiki.yml
+++ b/.github/workflows/wiki.yml
@@ -46,7 +46,7 @@
           go run ./tools/docs2wiki -w ./docs
           cd ..
           cd wiki
-          diff -ruN --exclude=.git . ../vscode-go/docs > ../mypatch || patch -p3 -E -f < ../mypatch
+          rm -r ./* && cp -r ../vscode-go/docs/* .
           git config --local user.email "action@github.com"
           git config --local user.name "GitHub Action"
           git add .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c237991..9c47efa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,26 @@
-## v0.36.0 - 4 Nov, 2022
+## v0.37.0 - 19 Dec, 2022
+
+This release includes new [static analysis features](https://github.com/golang/vscode-go/wiki/features#analyze-vulnerabilities-in-dependencies) that report known vulnerabilities in your dependencies. These vulncheck analysis tools are backed by [Go's vulnerability database](https://go.dev/security/vulndb) and the Go language server's integration of [`govulncheck`](https://golang.org/x/vuln/cmd/govulncheck").
+Read [Go's support for vulnerability management](https://go.dev/blog/vuln) to learn about the Go team's approach to helping Go developers secure their open-source dependencies.
+
+### Changes
+- The new "Go: Toggle Vulncheck" command enables/disables imports-based vulnerability analysis. This requires gopls v0.11.0 or newer.
+- Test and debug test code lenses are added to some subtests if the test names can be determined. ([Issue 2536](https://github.com/golang/vscode-go/issues/2536))
+- Gopls settings was updated to match gopls@v0.11.0.
+- `"go.formatTool"` setting accepts a special value `"custom"`, which causes the extension to use the custom formatter configured with the setting `"go.alternateTools": { "customFormatter": <your custom tool name> }`. ([Issue 2503](https://github.com/golang/vscode-go/issues/2503))
+- The experimental "Go: Run Vulncheck (exp)" command was removed.
+- The extension no longer bypasses Delve's Go version check by default. Users must install the delve version compatible with their Go version, or explicitly configure their launch configuration to pass the `--check-go-version=false` flag using the `dlvFlags` attribute. ([Go Delve Issue 3058](https://github.com/go-delve/delve/issues/3058))
+
+### Fixes
+- The editor survey prompt logic was adjusted for uniform sampling. ([Issue 2545](https://github.com/golang/vscode-go/issues/2545))
+- Fixed the crash bug when handling coverage profiles involving go `//line`-directive. ([Issue 2453](https://github.com/golang/vscode-go/issues/2453))
+- Updated dependencies to address [CVE-2022-37603](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-37603) and [CVE-2022-24999](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24999).
+
+### Thanks
+
+Thank you for your contribution, @devuo, @pjweinbgo, @aarzilli, @tklauser, @hyangah, @suzmue, @jamalc!
+
+## v0.36.0 - 7 Nov, 2022
 A list of all issues and changes can be found in the [v0.36.0 milestone](https://github.com/golang/vscode-go/milestone/52) and [commit history](https://github.com/golang/vscode-go/compare/v0.35.2...v0.36.0).
 
 ### Changes
diff --git a/docs/commands.md b/docs/commands.md
index 2752332..c9cd7a0 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -47,6 +47,10 @@
 
 Runs a sub test at the cursor.
 
+### `Go: Debug Subtest At Cursor`
+
+Debug a sub test at the cursor.
+
 ### `Go: Benchmark Function At Cursor`
 
 Runs a benchmark at the cursor.
@@ -151,6 +155,10 @@
 
 Toggles between file in current active editor and the corresponding test file.
 
+### `Go: Toggle Vulncheck`
+
+Toggle the display of vulnerability analysis in dependencies.
+
 ### `Go: Add Tags To Struct Fields`
 
 Add tags configured in go.addTags setting to selected struct using gomodifytags
@@ -266,7 +274,3 @@
 ### `Go: Reset Workspace Env`
 
 Reset the Go Env for the active workspace.
-
-### `Go: Run Vulncheck (Preview)`
-
-Run vulnerability check. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck for more details about the analysis.
diff --git a/docs/features.md b/docs/features.md
index 630b60e..9663cd6 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -16,6 +16,7 @@
   * [Document outline](#document-outline)
   * [Toggle between code and tests](#toggle-between-code-and-tests)
 * [Syntax Highlighting](#syntax-highlighting)
+* [Inlay Hints](#inlay-hints)
 * [Code Editing](#code-editing)
   * [Snippets](#snippets)
   * [Format and organize imports](#format-and-organize-imports)
@@ -29,8 +30,10 @@
   * [Fill struct literals](#fill-struct-literals)
 * [Diagnostics](#diagnostics)
   * [Build errors](#build-errors)
-  * [Vet errors](#vet-errors)
+  * [Vet and extra analyses](#vet-and-extra-analyses)
   * [Lint errors](#lint-errors)
+  * [Vulnerabilities in dependencies](#analyze-vulnerabilities-in-dependencies)
+* [Code Lenses](#code-lenses)
 * [Run and test in the editor](#run-and-test-in-the-editor)
   * [Run your code](#run-your-code)
   * [Test and benchmark](#test-and-benchmark)
@@ -105,7 +108,6 @@
 
 <div style="text-align: center;"><img src="images/toggletestfile.gif" alt="Toggle between reverse.go and reverse_test.go" style="width: 75%"> </div>
 
-
 ## Syntax Highlighting
 
 The default syntax highlighting for Go files is implemented in Visual Studio Code using TextMate grammar, not by this extension.
@@ -118,6 +120,55 @@
 
 <div style="text-align: center;"><img src="images/gotmpl.gif" alt="Enable Go template language support by changing the language ID" style="width: 75%"> </div>
 
+## Inlay Hints
+
+Inlay hints render additional inline information to source code to help you understand what the code does.
+They can be enabled/disabled with the `editor.inlayHints.enabled` setting in combination with settings to enable inlay hints types.
+
+### Variable types in assign statements
+
+```go
+	i/* int*/, j/* int*/ := 0, len(r)-1
+```
+
+### Variable types in range statements
+```go
+	for k/* int*/, v/* string*/ := range []string{} {
+		fmt.Println(k, v)
+	}
+```
+### Composite literal field names
+```go
+	{/*in: */"Hello, world", /*want: */"dlrow ,olleH"}
+```
+
+### Composite literal types
+```go
+	for _, c := range []struct {
+		in, want string
+	}{
+		/*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"},
+	}
+```
+### Constant values
+```go
+	const (
+		KindNone   Kind = iota/* = 0*/
+		KindPrint/*  = 1*/
+		KindPrintf/* = 2*/
+		KindErrorf/* = 3*/
+	)
+```
+### Function type parameters
+```go
+	myFoo/*[int, string]*/(1, "hello")
+```
+
+### Parameter names
+```go
+	parseInt(/* str: */ "123", /* radix: */ 8)
+```
+
 ## Code Editing
 
 ### [Snippets](https://code.visualstudio.com/docs/editor/userdefinedsnippets)
@@ -140,6 +191,11 @@
 
 <div style="text-align: center;"><img src="images/addimport.gif" alt="Add byte import to Go file" style="width: 75%"> </div>
 
+#### Custom formatter
+
+In addition to the default `gofmt`-style formatter, the Go language server supports `gofumpt`-style formatting. You can enable `gofumpt` formatting by setting `"gopls.formatting.gofumpt"`.
+You can  also configure to use other custom formatter by using the `"go.formatTool"` setting. The custom formatter must operate on file contents from STDIN, and output the formatted result to STDOUT.
+
 ### [Rename symbol](https://code.visualstudio.com/docs/editor/refactoring#_rename-symbol)
 
 Rename all occurrences of a symbol in your workspace.
@@ -185,29 +241,47 @@
 
 <div style="text-align: center;"><img src="images/fillstructliterals.gif" alt="Fill struct literals" style="width: 75%"> </div>
 
-## Diagnostics
+## Diagnostics 
 
-Learn more about [diagnostic errors](tools.md#diagnostics).
+The extension, powered by the Go language server (`gopls`), offers various diagnostics and analyses features,
+and often with quick fixes to address detected issues.
 
 ### Build errors
 
-Build errors can be shown as you type or on save. Configure this behavior through the [`"go.buildOnSave"`](settings.md#go.buildOnSave) setting.
+Compile and type errors are shown as you type by default. This works not only Go source code, but also `go.mod`, `go.work`, and Go template files.
 
-By default, code is compiled using the `go` command (`go build`), but build errors as you type are provided by the [`gotype-live`](tools.md#diagnostics) tool.
+### Vet and extra analyses
 
-### Vet errors
-
-Vet errors can be shown on save. The vet-on-save behavior can also be configured through the [`"go.vetOnSave"`](settings.md#go.vetOnSave) setting.
-
-The vet tool used is the one provided by the `go` command: [`go vet`](https://golang.org/cmd/vet/).
+The Go language server (`gopls`) reports [`vet`](https://pkg.go.dev/cmd/vet) errors and runs many useful analyzers as you type. A full list of analyzers that `gopls` uses can be found in the [analyses  settings section](https://github.com/golang/vscode-go/wiki/settings#uidiagnosticanalyses).
 
 ### Lint errors
 
-Much like vet errors, lint errors can also be shown on save. This behavior is configurable through the [`"go.lintOnSave"`](settings.md#go.lintOnSave) setting.
+You can configure an extra linter to run on file save. This behavior is configurable through the [`"go.lintOnSave"`](settings.md#go.lintOnSave) setting.
 
-The default lint tool is [`staticcheck`]. However, custom lint tools can be easily used instead by configuring the [`"go.lintTool"`](settings.md#go.lintTool) setting. [`golint`], [`golangci-lint`], and [`revive`] are also supported.
+The default lint tool is [`staticcheck`]. Popular alternative linters such as [`golint`], [`golangci-lint`] and [`revive`] can be used instead by configuring the [`"go.lintTool"`](settings.md#go.lintTool) setting. For a complete overview of linter options, see the [documentation for diagnostic tools](tools.md#diagnostics).
 
-For a complete overview of linter options, see the [documentation for diagnostic tools](tools.md#diagnostics).
+### Analyze vulnerabilities in dependencies
+
+The extension checks the 3rd party dependencies in your code and surfaces vulnerabilities known to the [Go vulnerability database](https://vuln.go.dev). There are two modes that complement each other.
+
+* Import-based analysis: this can be enabled using the [`"go.diagnostic.vulncheck": "Imports"`](settings.md#go.diagnostic.vulncheck) setting. You can turn on and off this analysis conveniently with the ["Go: Toggle Vulncheck"](commands.md#go-toggle-vulncheck) command. In this mode, `gopls` reports vulnerabilities that affect packages directly and indirectly used by your code. The diagnostics are reported in the `go.mod` file along with quick fixes to help upgrading vulnerable modules.
+
+* `Govulncheck` analysis: this is based on the [`golang.org/x/vuln/cmd/govulncheck`](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) tool, which is embedded in `gopls`. This provides a low-noise, reliable way to inspect known vulnerabilities. This only surfaces vulnerabilities that actually affect your code, based on which functions in your code are transitively calling vulnerable functions. This can be accessible by the `gopls` [`run_govulncheck`](settings.md#uicodelenses) code lens. The import-based analysis result also provides the `"Run govulncheck to verify"` option as a quick fix. 
+
+<div style="text-align: center;"><img src="images/vulncheck.gif" alt="Vulncheck">
+<em>Go: Toggle Vulncheck</em> <a href="https://user-images.githubusercontent.com/4999471/206977512-a821107d-9ffb-4456-9b27-6a6a4f900ba6.mp4">(vulncheck.mp4)</a> </div>
+
+These features require _`gopls` v0.11.0 or newer_.
+
+Please share your feedback at https://go.dev/s/vsc-vulncheck-feedback.
+Report a bug and feature request in [our issue tracker](https://github.com/golang/vscode-go/issues/new).
+
+**Notes and Caveats**
+
+- The import-based analysis uses the list of packages in the workspace modules, which may be different from what you see from `go.mod` files if `go.work` or module `replace`/`exclude` is used.
+- The govulncheck analysis result can become stale as you modify code or the Go vulnerability database is updated. In order to invalidate the analysis results manually, use the [`"Reset go.mod diagnostics"`] codelens shown on the top of the `go.mod` file. Otherwise, the result will be automatically invalidated after an hour.
+- These features currently don't report vulnerabilities in the standard libraries or tool chains. We are still investigating UX on where to surface the findings and how to help users handle the issues.
+- The extension does not scan private packages nor send any information on private modules. All the analysis is done by pulling a list of known vulnerable modules from the Go vulnerability database and then computing the intersection locally.
 
 ## Run and test in the editor
 
diff --git a/docs/images/vulncheck.gif b/docs/images/vulncheck.gif
new file mode 100644
index 0000000..4744092
--- /dev/null
+++ b/docs/images/vulncheck.gif
Binary files differ
diff --git a/docs/settings.md b/docs/settings.md
index 8958bde..baeefe4 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -51,9 +51,9 @@
 Alternate tools or alternate paths for the same tools used by the Go extension. Provide either absolute path or the name of the binary in GOPATH/bin, GOROOT/bin or PATH. Useful when you want to use wrapper script for the Go tools.
 | Properties | Description |
 | --- | --- |
+| `customFormatter` | Custom formatter to use instead of the language server. This should be used with the `custom` option in `#go.formatTool#`. <br/> Default: `""` |
 | `dlv` | Alternate tool to use instead of the dlv binary or alternate path to use for the dlv binary. <br/> Default: `"dlv"` |
 | `go` | Alternate tool to use instead of the go binary or alternate path to use for the go binary. <br/> Default: `"go"` |
-| `go-outline` | Alternate tool to use instead of the go-outline binary or alternate path to use for the go-outline binary. <br/> Default: `"go-outline"` |
 | `gopls` | Alternate tool to use instead of the gopls binary or alternate path to use for the gopls binary. <br/> Default: `"gopls"` |
 ### `go.autocompleteUnimportedPackages`
 
@@ -151,6 +151,18 @@
 | `showLog` | Show log output from the delve debugger. Maps to dlv's `--log` flag. <br/> Default: `false` |
 | `showRegisters` | Boolean value to indicate whether register variables should be shown in the variables pane or not. <br/> Default: `false` |
 | `substitutePath` | An array of mappings from a local path to the remote path that is used by the debuggee. The debug adapter will replace the local path with the remote path in all of the calls. Overriden by `remotePath` (in attach request). |
+### `go.diagnostic.vulncheck`
+
+(Experimental) vulncheck enables vulnerability scanning.
+<br/>
+Allowed Options:
+
+* `Imports`: `"Imports"`: In Imports mode, `gopls` will report vulnerabilities that affect packages
+directly and indirectly used by the analyzed main module.
+* `Off`: `"Off"`: Disable vulnerability analysis.
+
+
+Default: `"Off"`
 ### `go.disableConcurrentTests`
 
 If true, tests will not run concurrently. When a new test run is started, the previous will be cancelled.
@@ -221,8 +233,16 @@
 Flags to pass to format tool (e.g. ["-s"]). Not applicable when using the language server.
 ### `go.formatTool`
 
-When the language server is enabled and one of default/gofmt/goimports/gofumpt is chosen, the language server will handle formatting. Otherwise, the extension will use the specified tool for formatting.<br/>
-Allowed Options: `default`, `gofmt`, `goimports`, `goformat`, `gofumpt`
+When the language server is enabled and one of `default`/`gofmt`/`goimports`/`gofumpt` is chosen, the language server will handle formatting. If `custom` tool is selected, the extension will use the `customFormatter` tool in the `#go.alternateTools#` section.<br/>
+Allowed Options:
+
+* `default`: If the language server is enabled, format via the language server, which already supports gofmt, goimports, goreturns, and gofumpt. Otherwise, goimports.
+* `gofmt`: Formats the file according to the standard Go style. (not applicable when the language server is enabled)
+* `goimports`: Organizes imports and formats the file with gofmt. (not applicable when the language server is enabled)
+* `goformat`: Configurable gofmt, see https://github.com/mbenkmann/goformat.
+* `gofumpt`: Stricter version of gofmt, see https://github.com/mvdan/gofumpt. . Use `#gopls.format.gofumpt#` instead)
+* `custom`: Formats using the custom tool specified as `customFormatter` in the `#go.alternateTools#` setting. The tool should take the input as STDIN and output the formatted code as STDOUT.
+
 
 Default: `"default"`
 ### `go.generateTestsFlags`
@@ -651,17 +671,6 @@
 
 
 Default: `true`
-### `build.experimentalUseInvalidMetadata`
-
-(Experimental) experimentalUseInvalidMetadata enables gopls to fall back on outdated
-package metadata to provide editor features if the go command fails to
-load packages for some reason (like an invalid go.mod file).
-
-Deprecated: this setting is deprecated and will be removed in a future
-version of gopls (https://go.dev/issue/55333).
-
-
-Default: `false`
 ### `build.experimentalWorkspaceModule`
 
 (Experimental) experimentalWorkspaceModule opts a user into the experimental support
@@ -757,7 +766,7 @@
 | `gc_details` | Toggle the calculation of gc annotations. <br/> Default: `false` |
 | `generate` | Runs `go generate` for a given directory. <br/> Default: `true` |
 | `regenerate_cgo` | Regenerates cgo definitions. <br/> Default: `true` |
-| `run_vulncheck_exp` | Run vulnerability check (`govulncheck`). <br/> Default: `false` |
+| `run_govulncheck` | Run vulnerability check (`govulncheck`). <br/> Default: `false` |
 | `test` | Runs `go test` for a specific set of test or benchmark functions. <br/> Default: `false` |
 | `tidy` | Runs `go mod tidy` for a module. <br/> Default: `true` |
 | `upgrade_dependency` | Upgrades a dependency in the go.mod file for a module. <br/> Default: `true` |
@@ -832,7 +841,7 @@
 | `httpresponse` | check for mistakes using HTTP responses <br/> A common mistake when using the net/http package is to defer a function call to close the http.Response Body before checking the error that determines whether the response is valid: <br/> <pre>resp, err := http.Head(url)<br/>defer resp.Body.Close()<br/>if err != nil {<br/>	log.Fatal(err)<br/>}<br/>// (defer statement belongs here)</pre><br/> This checker helps uncover latent nil dereference bugs by reporting a diagnostic for such mistakes. <br/> Default: `true` |
 | `ifaceassert` | detect impossible interface-to-interface type assertions <br/> This checker flags type assertions v.(T) and corresponding type-switch cases in which the static type V of v is an interface that cannot possibly implement the target interface T. This occurs when V and T contain methods with the same name but different signatures. Example: <br/> <pre>var v interface {<br/>	Read()<br/>}<br/>_ = v.(io.Reader)</pre><br/> The Read method in v has a different signature than the Read method in io.Reader, so this assertion cannot succeed. <br/> <br/> Default: `true` |
 | `infertypeargs` | check for unnecessary type arguments in call expressions <br/> Explicit type arguments may be omitted from call expressions if they can be inferred from function arguments, or from other type arguments: <br/> <pre>func f[T any](T) {}<br/><br/><br/>func _() {<br/>	f[string]("foo") // string could be inferred<br/>}</pre><br/> <br/> Default: `true` |
-| `loopclosure` | check references to loop variables from within nested functions <br/> This analyzer checks for references to loop variables from within a function literal inside the loop body. It checks for patterns where access to a loop variable is known to escape the current loop iteration:  1. a call to go or defer at the end of the loop body  2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body <br/> The analyzer only considers references in the last statement of the loop body as it is not deep enough to understand the effects of subsequent statements which might render the reference benign. <br/> For example: <br/> <pre>for i, v := range s {<br/>	go func() {<br/>		println(i, v) // not what you might expect<br/>	}()<br/>}</pre><br/> See: https://golang.org/doc/go_faq.html#closures_and_goroutines <br/> Default: `true` |
+| `loopclosure` | check references to loop variables from within nested functions <br/> This analyzer reports places where a function literal references the iteration variable of an enclosing loop, and the loop calls the function in such a way (e.g. with go or defer) that it may outlive the loop iteration and possibly observe the wrong value of the variable. <br/> In this example, all the deferred functions run after the loop has completed, so all observe the final value of v. <br/>     for _, v := range list {         defer func() {             use(v) // incorrect         }()     } <br/> One fix is to create a new variable for each iteration of the loop: <br/>     for _, v := range list {         v := v // new var per iteration         defer func() {             use(v) // ok         }()     } <br/> The next example uses a go statement and has a similar problem. In addition, it has a data race because the loop updates v concurrent with the goroutines accessing it. <br/>     for _, v := range elem {         go func() {             use(v)  // incorrect, and a data race         }()     } <br/> A fix is the same as before. The checker also reports problems in goroutines started by golang.org/x/sync/errgroup.Group. A hard-to-spot variant of this form is common in parallel tests: <br/>     func Test(t *testing.T) {         for _, test := range tests {             t.Run(test.name, func(t *testing.T) {                 t.Parallel()                 use(test) // incorrect, and a data race             })         }     } <br/> The t.Parallel() call causes the rest of the function to execute concurrent with the loop. <br/> The analyzer reports references only in the last statement, as it is not deep enough to understand the effects of subsequent statements that might render the reference benign. ("Last statement" is defined recursively in compound statements such as if, switch, and select.) <br/> See: https://golang.org/doc/go_faq.html#closures_and_goroutines <br/> Default: `true` |
 | `lostcancel` | check cancel func returned by context.WithCancel is called <br/> The cancellation function returned by context.WithCancel, WithTimeout, and WithDeadline must be called or the new context will remain live until its parent context is cancelled. (The background context is never cancelled.) <br/> Default: `true` |
 | `nilfunc` | check for useless comparisons between functions and nil <br/> A useless comparison is one like f == nil as opposed to f() == nil. <br/> Default: `true` |
 | `nilness` | check for redundant or impossible nil comparisons <br/> The nilness checker inspects the control-flow graph of each function in a package and reports nil pointer dereferences, degenerate nil pointers, and panics with nil values. A degenerate comparison is of the form x==nil or x!=nil where x is statically known to be nil or non-nil. These are often a mistake, especially in control flow related to errors. Panics with nil values are checked because they are not detectable by <br/> <pre>if r := recover(); r != nil {</pre><br/> This check reports conditions such as: <br/> <pre>if f == nil { // impossible condition (f is a function)<br/>}</pre><br/> and: <br/> <pre>p := &v<br/>...<br/>if p != nil { // tautological condition<br/>}</pre><br/> and: <br/> <pre>if p == nil {<br/>	print(*p) // nil dereference<br/>}</pre><br/> and: <br/> <pre>if p == nil {<br/>	panic(p)<br/>}</pre><br/> <br/> Default: `false` |
diff --git a/media/vulncheckView.css b/media/vulncheckView.css
deleted file mode 100644
index 4531979..0000000
--- a/media/vulncheckView.css
+++ /dev/null
@@ -1,69 +0,0 @@
-/*---------------------------------------------------------
- * Copyright 2022 The Go Authors. All rights reserved.
- * Licensed under the MIT License. See LICENSE in the project root for license information.
- *--------------------------------------------------------*/
-
-.debug,
-.info {
-	font-weight: lighter;
-	padding-bottom: 1em;
-}
-
-.log,
-.vuln {
-	text-align: left;
-	padding-bottom: 1em;
-}
-
-.vuln-icon-info, .vuln-icon-warning {
-	padding-right: 1em;
-	font-size: 14px;
-	display: inline;
-}
-
-.vuln-icon-info {
-	color: var(--vscode-list-warningForeground);
-}
-
-.vuln-icon-warning {
-	color: var(--vscode-list-errorForeground);
-}
-
-.vuln-desc {
-	padding-top: 0.5em;
-	padding-bottom: 0.5em;
-}
-
-.vuln-details {
-	padding-bottom: 0.5em;
-}
-
-.vuln-fix:hover,
-.vuln-fix:active {
-	color: var(--vscode-textLink-activeForeground);
-}
-.vuln-fix {
-	cursor:pointer;
-	color: var(--vscode-textLink-foreground);
-	text-decoration:underline;
-}
-
-details summary {
-	cursor: pointer;
-	position: relative;
-}
-
-details summary>* {
-	display: inline;
-	position: relative;
-}
-
-.stacks {
-	padding: 1em;
-}
-
-.stack {
-	padding: 1em;
-	font-size: var(--vscode-editor-font-size);
-	font-family: var(--vscode-editor-font-family);
-}
\ No newline at end of file
diff --git a/media/vulncheckView.js b/media/vulncheckView.js
deleted file mode 100644
index a435f74..0000000
--- a/media/vulncheckView.js
+++ /dev/null
@@ -1,261 +0,0 @@
-/*---------------------------------------------------------
- * Copyright 2022 The Go Authors. All rights reserved.
- * Licensed under the MIT License. See LICENSE in the project root for license information.
- *--------------------------------------------------------*/
-
-// Script for VulncheckResultViewProvider's webview.
-
-(function () {
-
-	// @ts-ignore
-	const vscode = acquireVsCodeApi();
-
-	const logContainer = /** @type {HTMLElement} */ (document.querySelector('.log'));
-	const vulnsContainer = /** @type {HTMLElement} */ (document.querySelector('.vulns'));
-	const unaffectingContainer = /** @type {HTMLElement} */ (document.querySelector('.unaffecting'));
-	const debugContainer = /** @type {HTMLElement} */ (document.querySelector('.debug'));
-
-	vulnsContainer.addEventListener('click', (event) => {
-		let node = event && event.target;
-		let handled = false;
-		console.log(`${node.type} ${node.tagName} ${node.className} ${node.id} data:${node.dataset?.target} dir:${node.dataset?.dir}`);
-		if (node?.tagName === 'A' && node.href) {
-			// Ask vscode to handle link opening.
-			vscode.postMessage({ type: 'open', target: node.href });
-		} else if (node?.tagName === 'SPAN' && node.className === 'vuln-fix' && node.dataset?.target && node.dataset?.dir) {
-			vscode.postMessage({ type: 'fix', target: node.dataset?.target, dir: node.dataset?.dir });
-		}
-
-		if (handled) {
-			event.preventDefault();
-			event.stopPropagation();
-		}
-	});
-
-	const errorContainer = document.createElement('div');
-	document.body.appendChild(errorContainer);
-	errorContainer.className = 'error'
-	errorContainer.style.display = 'none'
-
-	function packageVersion(/** @type {string} */mod, /** @type {string} */pkg, /** @type {string|undefined} */ver) {
-		if (!ver) {
-			return 'N/A';
-		}
-
-		if (mod === 'stdlib' && ver.startsWith('v')) {
-			ver = `go${ver.slice(1)}`;
-		}
-		return `<a href="https://pkg.go.dev/${pkg}@${ver}">${pkg}@${ver}</a>`;
-	}
-
-	function modVersion(/** @type {string} */mod, /** @type {string|undefined} */ver) {
-		if (!ver) {
-			return 'N/A';
-		}
-
-		if (mod === 'stdlib' && ver.startsWith('v')) {
-			ver = `go${ver.slice(1)}`;
-		}
-		return `<a href="https://pkg.go.dev/${mod}@${ver}">${mod}@${ver}</a>`;
-	}
-
-	function offerUpgrade(/** @type {string} */dir, /** @type {string} */mod, /** @type {string|undefined} */ver) {
-		if (mod === 'stdlib') {
-			return '';
-		}
-		if (dir && mod && ver) {
-			return ` [<span class="vuln-fix" data-target="${mod}@${ver}" data-dir="${dir}">go get</span> | <span class="vuln-fix" data-target="${mod}@latest" data-dir="${dir}">go get latest</span>]`
-		}
-		return '';
-	}
-
-	function snapshotContent() {
-		const res = {
-			'log': logContainer.innerHTML,
-			'vulns': vulnsContainer.innerHTML,
-			'unaffecting': unaffectingContainer.innerHTML
-		};
-		return JSON.stringify(res);
-	}
-
-	/**
-	 * Render the document in the webview.
-	 */
-	function updateContent(/** @type {string} */ text = '{}') {
-		let json;
-		try {
-			json = JSON.parse(text);
-		} catch {
-			errorContainer.innerText = 'Error: Document is not valid json';
-			errorContainer.style.display = '';
-			return;
-		}
-		errorContainer.style.display = 'none';
-
-		const timeinfo = (startDate, durationMillisec) => {
-			if (!startDate) { return '' }
-			return durationMillisec ? `${startDate} (took ${durationMillisec} msec)` : `${startDate}`;
-		}
-		debugContainer.innerHTML = `Analyzed at: ${timeinfo(json.Start, json.Duration)}`;
-
-		const vulns = json.Vuln || [];
-		const affecting = vulns.filter((v) => v.CallStackSummaries?.length);
-		const unaffecting = vulns.filter((v) => !v.CallStackSummaries?.length);
-
-		logContainer.innerHTML = `
-<pre>cd ${json.Dir || ''}; govulncheck ${json.Pattern || ''}</pre>
-Found ${affecting?.length || 0} known vulnerabilities.`;
-		
-		vulnsContainer.innerHTML = '';
-		affecting.forEach((vuln) => {
-			const element = document.createElement('div');
-			element.className = 'vuln';
-			vulnsContainer.appendChild(element);
-
-			// TITLE - Vuln ID
-			const title = document.createElement('h2');
-			title.innerHTML = `<div class="vuln-icon-warning"><i class="codicon codicon-warning"></i></div><a href="${vuln.URL}">${vuln.ID}</a>`;
-			title.className = 'vuln-title';
-			element.appendChild(title);
-
-			// DESCRIPTION - short text (aliases)
-			const desc = document.createElement('p');
-			desc.innerHTML = Array.isArray(vuln.Aliases) && vuln.Aliases.length ? `${vuln.Details} (${vuln.Aliases.join(', ')})` : vuln.Details;
-			desc.className = 'vuln-desc';
-			element.appendChild(desc);
-
-			// DETAILS - dump of all details
-			const details = document.createElement('table');
-			details.className = 'vuln-details'
-			details.innerHTML = `
-			<tr><td>Package</td><td>${vuln.PkgPath}</td></tr>
-			<tr><td>Found in Version</td><td>${packageVersion(vuln.ModPath, vuln.PkgPath, vuln.CurrentVersion)}</td></tr>
-			<tr><td>Fixed Version</td><td>${packageVersion(vuln.ModPath, vuln.PkgPath, vuln.FixedVersion)} ${offerUpgrade(json.Dir, vuln.ModPath, vuln.FixedVersion)}</td></tr>
-			<tr><td>Affecting</td><td>${vuln.AffectedPkgs?.join('<br>')}</td></tr>
-			`;
-			element.appendChild(details);
-
-			/* TODO: Action for module version upgrade */
-			/* TODO: Explain module dependency - why am I depending on this vulnerable version? */
-
-			// EXEMPLARS - call stacks (initially hidden)
-			const examples = document.createElement('details');
-			examples.innerHTML = `<summary>${vuln.CallStackSummaries?.length || 0}+ findings</summary>`;
-
-			// Call stacks
-			const callstacksContainer = document.createElement('p');
-			callstacksContainer.className = 'stacks';
-			vuln.CallStackSummaries?.forEach((summary, idx) => {
-				const callstack = document.createElement('details');
-				const s = document.createElement('summary');
-				s.innerText = summary;
-				callstack.appendChild(s);
-
-				const stack = document.createElement('div');
-				stack.className = 'stack';
-				const cs = vuln.CallStacks[idx];
-				cs.forEach((c) => {
-					const p = document.createElement('p');
-					const pos = c.URI ? `${c.URI}?${c.Pos.line || 0}` : '';
-					p.innerHTML = pos ? `<a href="${pos}">${c.Name}</a>` : c.Name;
-					stack.appendChild(p);
-				});
-				callstack.appendChild(stack);
-
-				callstacksContainer.appendChild(callstack);
-			})
-
-			examples.appendChild(callstacksContainer);
-			element.appendChild(examples);
-		});
-
-		unaffectingContainer.innerText = '';
-		if (unaffecting.length > 0) {
-			const notice = document.createElement('div');
-			notice.className = 'info';
-			notice.innerHTML = `
-<hr></hr>The vulnerabilities below are in packages that you import, 
-but your code does not appear to call any vulnerable functions. 
-You may not need to take any action. See 
-<a href="https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck">
-https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck</a>
-for details.
-`;
-
-			unaffectingContainer.appendChild(notice);
-
-			unaffecting.forEach((vuln) => {
-				const element = document.createElement('div');
-				element.className = 'vuln';
-				unaffectingContainer.appendChild(element);
-
-				// TITLE - Vuln ID
-				const title = document.createElement('h2');
-				title.innerHTML = `<div class="vuln-icon-info"><i class="codicon codicon-info"></i></div><a href="${vuln.URL}">${vuln.ID}</a>`;
-				title.className = 'vuln-title';
-				element.appendChild(title);
-
-				// DESCRIPTION - short text (aliases)
-				const desc = document.createElement('p');
-				desc.innerHTML = Array.isArray(vuln.Aliases) && vuln.Aliases.length ? `${vuln.Details} (${vuln.Aliases.join(', ')})` : vuln.Details;
-				desc.className = 'vuln-desc';
-				element.appendChild(desc);
-
-				// DETAILS - dump of all details
-				// TODO(hyangah):
-				//   - include the current version & package name when gopls provides them.
-				//   - offer upgrade like affect vulnerabilities. We will need to install another event listener
-				//     on unaffectingContainer. See vulnsContainer.addEventListener.
-				const details = document.createElement('table');
-				details.className = 'vuln-details'
-				if (vuln.FixedVersion) {
-					details.innerHTML = `<tr><td>Fixed Version</td><td>${modVersion(vuln.ModPath, vuln.FixedVersion)}</td></tr>`;
-				} else {
-					details.innerHTML = `<tr><td>Fixed Version</td><td>unavailable for ${vuln.ModPath}</td></tr>`;
-				}
-				element.appendChild(details);
-			});
-		}
-	}
-
-	// Message Passing between Extension and Webview
-	//
-	//  Extension sends 'update' to Webview to trigger rerendering.
-	//  Webview sends 'link' to Extension to forward all link
-	//     click events so the extension can handle the event.
-	//
-	//  Extension sends 'snapshot-request' to trigger dumping
-	//     of the current DOM in the 'vulns' container.
-	//  Webview sends 'snapshot-result' to the extension
-	//     as the response to snapshot-request.
-
-	// Handle messages sent from the extension to the webview
-	window.addEventListener('message', event => {
-		const message = event.data; // The json data that the extension sent
-		switch (message.type) {
-			case 'update':
-				const text = message.text;
-
-				updateContent(text);
-				// Then persist state information.
-				// This state is returned in the call to `vscode.getState` below when a webview is reloaded.
-				vscode.setState({ text });
-				return;
-			// Message for testing. Returns a current DOM in a serialized format.
-			case 'snapshot-request':
-				const result = snapshotContent();
-				vscode.postMessage({ type: 'snapshot-result', target: result });
-				return;
-		}
-	});
-
-	// Webviews are normally torn down when not visible and re-created when they become visible again.
-	// State lets us save information across these re-loads
-	const state = vscode.getState();
-	if (state) {
-		updateContent(state.text);
-	};
-	// TODO: Handle 'details' expansion info and store the state using
-	// vscode.setState or retainContextWhenHidden. Currently, we are storing only
-	// the document text. (see windowEventHandler)
-}());
diff --git a/package-lock.json b/package-lock.json
index b6a149a..4139718 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "go",
-  "version": "0.36.0",
+  "version": "0.37.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "go",
-      "version": "0.36.0",
+      "version": "0.37.0-dev",
       "license": "MIT",
       "dependencies": {
         "@vscode/codicons": "0.0.32",
@@ -2692,9 +2692,9 @@
       "dev": true
     },
     "node_modules/loader-utils": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
-      "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
+      "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
       "dev": true,
       "dependencies": {
         "big.js": "^5.2.2",
@@ -3477,9 +3477,9 @@
       }
     },
     "node_modules/qs": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
+      "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
       "engines": {
         "node": ">=0.6"
       }
@@ -6640,9 +6640,9 @@
       "dev": true
     },
     "loader-utils": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
-      "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
+      "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
       "dev": true,
       "requires": {
         "big.js": "^5.2.2",
@@ -7222,9 +7222,9 @@
       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
     },
     "qs": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
+      "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="
     },
     "queue-microtask": {
       "version": "1.2.3",
diff --git a/package.json b/package.json
index 03d9bb0..6233d84 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "go",
   "displayName": "Go",
-  "version": "0.36.0",
+  "version": "0.37.0",
   "publisher": "golang",
   "description": "Rich Go language support for Visual Studio Code",
   "author": {
@@ -171,6 +171,12 @@
         "aliases": [
           "Go Template File"
         ]
+      },
+      {
+        "id": "govulncheck",
+        "aliases": [
+          "Govulncheck Output"
+        ]
       }
     ],
     "grammars": [
@@ -188,6 +194,11 @@
         "language": "go.sum",
         "scopeName": "go.sum",
         "path": "./syntaxes/go.sum.tmGrammar.json"
+      },
+      {
+        "language": "govulncheck",
+        "scopeName": "govulncheck",
+        "path": "./syntaxes/govulncheck.tmGrammar.json"
       }
     ],
     "snippets": [
@@ -238,6 +249,11 @@
         "description": "Runs a sub test at the cursor."
       },
       {
+        "command": "go.debug.subtest.cursor",
+        "title": "Go: Debug Subtest At Cursor",
+        "description": "Debug a sub test at the cursor."
+      },
+      {
         "command": "go.benchmark.cursor",
         "title": "Go: Benchmark Function At Cursor",
         "description": "Runs a benchmark at the cursor."
@@ -374,6 +390,11 @@
         "description": "Toggles between file in current active editor and the corresponding test file."
       },
       {
+        "command": "go.vulncheck.toggle",
+        "title": "Go: Toggle Vulncheck",
+        "description": "Toggle the display of vulnerability analysis in dependencies."
+      },
+      {
         "command": "go.add.tags",
         "title": "Go: Add Tags To Struct Fields",
         "description": "Add tags configured in go.addTags setting to selected struct using gomodifytags"
@@ -525,12 +546,6 @@
         "description": "Reset the Go Env for the active workspace.",
         "icon": "$(settings-remove)",
         "enablement": "workspaceFolderCount > 0"
-      },
-      {
-        "command": "go.vulncheck.run",
-        "title": "Go: Run Vulncheck (Preview)",
-        "description": "Run vulnerability check. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck for more details about the analysis.",
-        "enablement": "go.goplsIsRunning"
       }
     ],
     "breakpoints": [
@@ -1205,23 +1220,23 @@
         "go.formatTool": {
           "type": "string",
           "default": "default",
-          "description": "When the language server is enabled and one of default/gofmt/goimports/gofumpt is chosen, the language server will handle formatting. Otherwise, the extension will use the specified tool for formatting.",
+          "markdownDescription": "When the language server is enabled and one of `default`/`gofmt`/`goimports`/`gofumpt` is chosen, the language server will handle formatting. If `custom` tool is selected, the extension will use the `customFormatter` tool in the `#go.alternateTools#` section.",
           "scope": "resource",
           "enum": [
             "default",
             "gofmt",
             "goimports",
             "goformat",
-            "gofumpt"
+            "gofumpt",
+            "custom"
           ],
-          "additionalItems": true,
-          "enumDescriptions": [
+          "markdownEnumDescriptions": [
             "If the language server is enabled, format via the language server, which already supports gofmt, goimports, goreturns, and gofumpt. Otherwise, goimports.",
             "Formats the file according to the standard Go style. (not applicable when the language server is enabled)",
             "Organizes imports and formats the file with gofmt. (not applicable when the language server is enabled)",
             "Configurable gofmt, see https://github.com/mbenkmann/goformat.",
-            "Stricter version of gofmt, see https://github.com/mvdan/gofumpt. (not applicable when the language server is enabled)",
-            "Applies gofumpt formatting and organizes imports."
+            "Stricter version of gofmt, see https://github.com/mvdan/gofumpt. . Use `#gopls.format.gofumpt#` instead)",
+            "Formats using the custom tool specified as `customFormatter` in the `#go.alternateTools#` setting. The tool should take the input as STDIN and output the formatted code as STDOUT."
           ]
         },
         "go.formatFlags": {
@@ -2029,15 +2044,15 @@
               "default": "gopls",
               "description": "Alternate tool to use instead of the gopls binary or alternate path to use for the gopls binary."
             },
-            "go-outline": {
-              "type": "string",
-              "default": "go-outline",
-              "description": "Alternate tool to use instead of the go-outline binary or alternate path to use for the go-outline binary."
-            },
             "dlv": {
               "type": "string",
               "default": "dlv",
               "description": "Alternate tool to use instead of the dlv binary or alternate path to use for the dlv binary."
+            },
+            "customFormatter": {
+              "type": "string",
+              "default": "",
+              "markdownDescription": "Custom formatter to use instead of the language server. This should be used with the `custom` option in `#go.formatTool#`."
             }
           },
           "additionalProperties": true
@@ -2096,12 +2111,6 @@
               "default": true,
               "scope": "resource"
             },
-            "build.experimentalUseInvalidMetadata": {
-              "type": "boolean",
-              "markdownDescription": "(Experimental) experimentalUseInvalidMetadata enables gopls to fall back on outdated\npackage metadata to provide editor features if the go command fails to\nload packages for some reason (like an invalid go.mod file).\n\nDeprecated: this setting is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55333).\n",
-              "default": false,
-              "scope": "resource"
-            },
             "build.experimentalWorkspaceModule": {
               "type": "boolean",
               "markdownDescription": "(Experimental) experimentalWorkspaceModule opts a user into the experimental support\nfor multi-module workspaces.\n\nDeprecated: this feature is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55331).\n",
@@ -2168,7 +2177,7 @@
                   "markdownDescription": "Regenerates cgo definitions.",
                   "default": true
                 },
-                "run_vulncheck_exp": {
+                "run_govulncheck": {
                   "type": "boolean",
                   "markdownDescription": "Run vulnerability check (`govulncheck`).",
                   "default": false
@@ -2326,7 +2335,7 @@
                 },
                 "loopclosure": {
                   "type": "boolean",
-                  "markdownDescription": "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n\nThe analyzer only considers references in the last statement of the loop body\nas it is not deep enough to understand the effects of subsequent statements\nwhich might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines",
+                  "markdownDescription": "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v.\n\n    for _, v := range list {\n        defer func() {\n            use(v) // incorrect\n        }()\n    }\n\nOne fix is to create a new variable for each iteration of the loop:\n\n    for _, v := range list {\n        v := v // new var per iteration\n        defer func() {\n            use(v) // ok\n        }()\n    }\n\nThe next example uses a go statement and has a similar problem.\nIn addition, it has a data race because the loop updates v\nconcurrent with the goroutines accessing it.\n\n    for _, v := range elem {\n        go func() {\n            use(v)  // incorrect, and a data race\n        }()\n    }\n\nA fix is the same as before. The checker also reports problems\nin goroutines started by golang.org/x/sync/errgroup.Group.\nA hard-to-spot variant of this form is common in parallel tests:\n\n    func Test(t *testing.T) {\n        for _, test := range tests {\n            t.Run(test.name, func(t *testing.T) {\n                t.Parallel()\n                use(test) // incorrect, and a data race\n            })\n        }\n    }\n\nThe t.Parallel() call causes the rest of the function to execute\nconcurrent with the loop.\n\nThe analyzer reports references only in the last statement,\nas it is not deep enough to understand the effects of subsequent\nstatements that might render the reference benign.\n(\"Last statement\" is defined recursively in compound\nstatements such as if, switch, and select.)\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines",
                   "default": true
                 },
                 "lostcancel": {
@@ -2624,6 +2633,20 @@
             }
           }
         },
+        "go.diagnostic.vulncheck": {
+          "type": "string",
+          "markdownDescription": "(Experimental) vulncheck enables vulnerability scanning.\n",
+          "enum": [
+            "Imports",
+            "Off"
+          ],
+          "markdownEnumDescriptions": [
+            "`\"Imports\"`: In Imports mode, `gopls` will report vulnerabilities that affect packages\ndirectly and indirectly used by the analyzed main module.\n",
+            "`\"Off\"`: Disable vulnerability analysis.\n"
+          ],
+          "default": "Off",
+          "scope": "resource"
+        },
         "go.inlayHints.assignVariableTypes": {
           "type": "boolean",
           "markdownDescription": "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```",
@@ -2837,17 +2860,6 @@
           "when": "go.hasProfiles"
         }
       ]
-    },
-    "customEditors": [
-      {
-        "viewType": "vulncheck.view",
-        "displayName": "Vulnerability Report",
-        "selector": [
-          {
-            "filenamePattern": "*.vulncheck.json"
-          }
-        ]
-      }
-    ]
+    }
   }
 }
\ No newline at end of file
diff --git a/src/context.ts b/src/context.ts
index 27f5ff4..e5562cf 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -16,14 +16,16 @@
 	languageClient?: LanguageClient;
 	legacyLanguageService?: LegacyLanguageService;
 	latestConfig?: LanguageServerConfig;
-	serverOutputChannel?: vscode.OutputChannel;
+	serverOutputChannel?: vscode.OutputChannel; // server-side output.
+	serverTraceChannel?: vscode.OutputChannel; // client-side tracing.
+	govulncheckOutputChannel?: vscode.OutputChannel; // govulncheck output.
+
 	languageServerIsRunning?: boolean;
 	// serverInfo is the information from the server received during initialization.
 	serverInfo?: ServerInfo;
 	// lastUserAction is the time of the last user-triggered change.
 	// A user-triggered change is a didOpen, didChange, didSave, or didClose event.
 	lastUserAction?: Date;
-	serverTraceChannel?: vscode.OutputChannel;
 	crashCount?: number;
 	// Some metrics for automated issue reports:
 	restartHistory?: Restart[];
diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts
index cda659f..3ea9361 100644
--- a/src/debugAdapter/goDebug.ts
+++ b/src/debugAdapter/goDebug.ts
@@ -2678,7 +2678,7 @@
 		if (errorMessage === 'bad access') {
 			// Reuse the panic message from the Go runtime.
 			errorMessage =
-				'runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation]\nUnable to propogate EXC_BAD_ACCESS signal to target process and panic (see https://github.com/go-delve/delve/issues/852)';
+				'runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation]\nUnable to propagate EXC_BAD_ACCESS signal to target process and panic (see https://github.com/go-delve/delve/issues/852)';
 		}
 
 		logError(message + ' - ' + errorMessage);
diff --git a/src/goCover.ts b/src/goCover.ts
index 24078e8..53a78a2 100644
--- a/src/goCover.ts
+++ b/src/goCover.ts
@@ -261,13 +261,25 @@
 
 				// and fill in coveragePath
 				const coverage = coveragePath.get(parse[1]) || emptyCoverageData();
+				// When line directive is used this information is artificial and
+				// the source code file can be non-existent or wrong (go.dev/issues/41222).
+				// There is no perfect way to guess whether the line/col in coverage profile
+				// is bogus. At least, we know that 0 or negative values are not true line/col.
+				const startLine = parseInt(parse[2], 10);
+				const startCol = parseInt(parse[3], 10);
+				const endLine = parseInt(parse[4], 10);
+				const endCol = parseInt(parse[5], 10);
+				if (startLine < 1 || startCol < 1 || endLine < 1 || endCol < 1) {
+					return;
+				}
 				const range = new vscode.Range(
 					// Convert lines and columns to 0-based
-					parseInt(parse[2], 10) - 1,
-					parseInt(parse[3], 10) - 1,
-					parseInt(parse[4], 10) - 1,
-					parseInt(parse[5], 10) - 1
+					startLine - 1,
+					startCol - 1,
+					endLine - 1,
+					endCol - 1
 				);
+
 				const counts = parseInt(parse[7], 10);
 				// If is Covered (CoverCount > 0)
 				if (counts > 0) {
diff --git a/src/goDebugFactory.ts b/src/goDebugFactory.ts
index 4972ab0..ae82e2e 100644
--- a/src/goDebugFactory.ts
+++ b/src/goDebugFactory.ts
@@ -643,13 +643,8 @@
 	const dlvArgs = new Array<string>();
 	dlvArgs.push('dap');
 
-	// TODO(hyangah): if Go version is higher than what the delve can support, we need to warn users.
-
 	// When duplicate flags are specified,
 	// dlv doesn't mind but accepts the last flag value.
-	// Add user-specified dlv flags first except
-	//  --check-go-version that we want to disable by default but allow users to override.
-	dlvArgs.push('--check-go-version=false');
 	if (launchAttachArgs.dlvFlags && launchAttachArgs.dlvFlags.length > 0) {
 		dlvArgs.push(...launchAttachArgs.dlvFlags);
 	}
diff --git a/src/goEnvironmentStatus.ts b/src/goEnvironmentStatus.ts
index 7902b84..d086f4f 100644
--- a/src/goEnvironmentStatus.ts
+++ b/src/goEnvironmentStatus.ts
@@ -187,8 +187,11 @@
 	// prompt the user to reload the window.
 	// promptReload defaults to true and should only be false for tests.
 	if (promptReload) {
-		const choice = await vscode.window.showInformationMessage(
+		const choice = await vscode.window.showWarningMessage(
 			'Please reload the window to finish applying Go version changes.',
+			{
+				modal: true
+			},
 			'Reload Window'
 		);
 		if (choice === 'Reload Window') {
diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts
index d607d57..b09d2a2 100644
--- a/src/goInstallTools.ts
+++ b/src/goInstallTools.ts
@@ -374,6 +374,12 @@
 
 export async function promptForMissingTool(toolName: string) {
 	const tool = getTool(toolName);
+	if (!tool) {
+		vscode.window.showWarningMessage(
+			`${toolName} is not found. Please make sure it is installed and available in the PATH ${envPath}`
+		);
+		return;
+	}
 
 	// If user has declined to install this tool, don't prompt for it.
 	if (declinedToolInstall(toolName)) {
@@ -444,6 +450,9 @@
 	message?: string
 ) {
 	const tool = getTool(toolName);
+	if (!tool) {
+		return; // not a tool known to us.
+	}
 	const toolVersion = { ...tool, version: newVersion }; // ToolWithVersion
 
 	// If user has declined to update, then don't prompt.
@@ -737,9 +746,9 @@
 			dep     github.com/BurntSushi/toml      v0.3.1  h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 
 		   if the binary was built with a dev version of go, in module mode.
-		    /Users/hakim/go/bin/gopls: devel go1.18-41f485b9a7 Mon Jan 31 13:43:52 2022 +0000
+			/Users/hakim/go/bin/gopls: devel go1.18-41f485b9a7 Mon Jan 31 13:43:52 2022 +0000
 			path    golang.org/x/tools/gopls
-            mod     golang.org/x/tools/gopls        v0.8.0-pre.1    h1:6iHi9bCJ8XndQtBEFFG/DX+eTJrf2lKFv4GI3zLeDOo=
+			mod     golang.org/x/tools/gopls        v0.8.0-pre.1    h1:6iHi9bCJ8XndQtBEFFG/DX+eTJrf2lKFv4GI3zLeDOo=
 			...
 		*/
 		const lines = stdout.split('\n', 3);
diff --git a/src/goMain.ts b/src/goMain.ts
index 873f2eb..dd413ac 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -64,10 +64,9 @@
 import { GoTestExplorer, isVscodeTestingAPIAvailable } from './goTest/explore';
 import { killRunningPprof } from './goTest/profile';
 import { GoExplorerProvider } from './goExplorer';
-import { VulncheckProvider, VulncheckResultViewProvider } from './goVulncheck';
-
 import { GoExtensionContext } from './context';
 import * as commands from './commands';
+import { toggleVulncheckCommandFactory, VulncheckOutputLinkProvider } from './goVulncheck';
 
 const goCtx: GoExtensionContext = {};
 
@@ -146,8 +145,9 @@
 	registerCommand('go.godoctor.var', commands.extractVariable);
 	registerCommand('go.test.cursor', commands.testAtCursor('test'));
 	registerCommand('go.test.cursorOrPrevious', commands.testAtCursorOrPrevious('test'));
-	registerCommand('go.subtest.cursor', commands.subTestAtCursor);
+	registerCommand('go.subtest.cursor', commands.subTestAtCursor('test'));
 	registerCommand('go.debug.cursor', commands.testAtCursor('debug'));
+	registerCommand('go.debug.subtest.cursor', commands.subTestAtCursor('debug'));
 	registerCommand('go.benchmark.cursor', commands.testAtCursor('benchmark'));
 	registerCommand('go.test.package', commands.testCurrentPackage(false));
 	registerCommand('go.benchmark.package', commands.testCurrentPackage(true));
@@ -170,8 +170,6 @@
 	}
 
 	GoExplorerProvider.setup(ctx);
-	VulncheckProvider.setup(ctx, goCtx);
-	VulncheckResultViewProvider.register(ctx, goCtx);
 
 	registerCommand('go.test.generate.package', goGenerateTests.generateTestCurrentPackage);
 	registerCommand('go.test.generate.file', goGenerateTests.generateTestCurrentFile);
@@ -212,6 +210,10 @@
 		wordPattern: /(-?\d*\.\d\w*)|([^`~!@#%^&*()\-=+[{\]}\\|;:'",.<>/?\s]+)/g
 	});
 
+	// Vulncheck output link provider.
+	VulncheckOutputLinkProvider.activate(ctx);
+	registerCommand('go.vulncheck.toggle', toggleVulncheckCommandFactory);
+
 	return extensionAPI;
 }
 
diff --git a/src/goRunTestCodelens.ts b/src/goRunTestCodelens.ts
index e17aa2c..0baa8ee 100644
--- a/src/goRunTestCodelens.ts
+++ b/src/goRunTestCodelens.ts
@@ -95,27 +95,57 @@
 
 	private async getCodeLensForFunctions(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
 		const testPromise = async (): Promise<CodeLens[]> => {
+			const codelens: CodeLens[] = [];
+
 			const testFunctions = await getTestFunctions(this.goCtx, document, token);
 			if (!testFunctions) {
-				return [];
+				return codelens;
 			}
-			const codelens: CodeLens[] = [];
+
+			const simpleRunRegex = /t.Run\("([^"]+)",/;
+
 			for (const f of testFunctions) {
+				const functionName = f.name;
+
 				codelens.push(
 					new CodeLens(f.range, {
 						title: 'run test',
 						command: 'go.test.cursor',
-						arguments: [{ functionName: f.name }]
-					})
-				);
-				codelens.push(
+						arguments: [{ functionName }]
+					}),
 					new CodeLens(f.range, {
 						title: 'debug test',
 						command: 'go.debug.cursor',
-						arguments: [{ functionName: f.name }]
+						arguments: [{ functionName }]
 					})
 				);
+
+				for (let i = f.range.start.line; i < f.range.end.line; i++) {
+					const line = document.lineAt(i);
+					const simpleMatch = line.text.match(simpleRunRegex);
+
+					// BUG: this does not handle nested subtests. This should
+					// be solved once codelens is handled by gopls and not by
+					// vscode.
+					if (simpleMatch) {
+						const subTestName = simpleMatch[1];
+
+						codelens.push(
+							new CodeLens(line.range, {
+								title: 'run test',
+								command: 'go.subtest.cursor',
+								arguments: [{ functionName, subTestName }]
+							}),
+							new CodeLens(line.range, {
+								title: 'debug test',
+								command: 'go.debug.subtest.cursor',
+								arguments: [{ functionName, subTestName }]
+							})
+						);
+					}
+				}
 			}
+
 			return codelens;
 		};
 
diff --git a/src/goSurvey.ts b/src/goSurvey.ts
index a06f1d4..2e9654e 100644
--- a/src/goSurvey.ts
+++ b/src/goSurvey.ts
@@ -7,20 +7,21 @@
 'use strict';
 
 import vscode = require('vscode');
-import { getLocalGoplsVersion } from './language/goLanguageServer';
-import { outputChannel } from './goStatus';
+import { CommandFactory } from './commands';
+import { getGoConfig } from './config';
 import { extensionId } from './const';
-import { getFromGlobalState, getFromWorkspaceState, updateGlobalState } from './stateUtils';
+import { GoExtensionContext } from './context';
 import {
 	developerSurveyConfig,
 	getDeveloperSurveyConfig,
 	maybePromptForDeveloperSurvey,
 	promptForDeveloperSurvey
 } from './goDeveloperSurvey';
-import { getGoConfig } from './config';
+import { outputChannel } from './goStatus';
+import { getLocalGoplsVersion } from './language/goLanguageServer';
+import { getFromGlobalState, getFromWorkspaceState, updateGlobalState } from './stateUtils';
 import { getGoVersion } from './util';
-import { GoExtensionContext } from './context';
-import { CommandFactory } from './commands';
+import { promptNext4Weeks } from './utils/randomDayutils';
 
 // GoplsSurveyConfig is the set of global properties used to determine if
 // we should prompt a user to take the gopls survey.
@@ -35,7 +36,7 @@
 	promptThisMonth?: boolean;
 
 	// dateToPromptThisMonth is the date on which we should prompt the user
-	// this month.
+	// this month. (It is no longer necessarily in the current month.)
 	dateToPromptThisMonth?: Date;
 
 	// dateComputedPromptThisMonth is the date on which the values of
@@ -113,25 +114,20 @@
 	if (cfg.dateComputedPromptThisMonth) {
 		// The extension has been activated this month, so we should have already
 		// decided if the user should be prompted.
-		if (daysBetween(now, cfg.dateComputedPromptThisMonth) < 30) {
+		if (daysBetween(now, cfg.dateComputedPromptThisMonth) < 28) {
 			return cfg;
 		}
 	}
 	// This is the first activation this month (or ever), so decide if we
 	// should prompt the user. This is done by generating a random number in
 	// the range [0, 1) and checking if it is < probability.
-	// We then randomly pick a day in the rest of the month on which to prompt
-	// the user.
+	// We then randomly pick a day in the next 4 weeks to prompt the user.
 	// Probability is set based on the # of responses received, and will be
 	// decreased if we begin receiving > 200 responses/month.
 	const probability = 0.06;
 	cfg.promptThisMonth = Math.random() < probability;
 	if (cfg.promptThisMonth) {
-		// end is the last day of the month, day is the random day of the
-		// month on which to prompt.
-		const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
-		const day = randomIntInRange(now.getUTCDate(), end.getUTCDate());
-		cfg.dateToPromptThisMonth = new Date(now.getFullYear(), now.getMonth(), day);
+		cfg.dateToPromptThisMonth = promptNext4Weeks(now);
 	} else {
 		cfg.dateToPromptThisMonth = undefined;
 	}
@@ -140,13 +136,6 @@
 	return cfg;
 }
 
-// randomIntInRange returns a random integer between min and max, inclusive.
-function randomIntInRange(min: number, max: number): number {
-	const low = Math.ceil(min);
-	const high = Math.floor(max);
-	return Math.floor(Math.random() * (high - low + 1)) + low;
-}
-
 async function promptForGoplsSurvey(
 	goCtx: GoExtensionContext,
 	cfg: GoplsSurveyConfig = {},
diff --git a/src/goTest.ts b/src/goTest.ts
index dba33ff..5d089bc 100644
--- a/src/goTest.ts
+++ b/src/goTest.ts
@@ -11,6 +11,7 @@
 import { getGoConfig } from './config';
 import { GoExtensionContext } from './context';
 import { isModSupported } from './goModules';
+import { escapeSubTestName } from './subTestUtils';
 import {
 	extractInstanceTestName,
 	findAllTestSuiteRuns,
@@ -33,6 +34,7 @@
 let lastDebugWorkspaceFolder: vscode.WorkspaceFolder | undefined;
 
 export type TestAtCursorCmd = 'debug' | 'test' | 'benchmark';
+export type SubTestAtCursorCmd = Exclude<TestAtCursorCmd, 'benchmark'>;
 
 class NotFoundError extends Error {}
 
@@ -73,6 +75,81 @@
 	}
 }
 
+async function _subTestAtCursor(
+	goCtx: GoExtensionContext,
+	goConfig: vscode.WorkspaceConfiguration,
+	cmd: SubTestAtCursorCmd,
+	args: any
+) {
+	const editor = vscode.window.activeTextEditor;
+	if (!editor) {
+		vscode.window.showInformationMessage('No editor is active.');
+		return;
+	}
+	if (!editor.document.fileName.endsWith('_test.go')) {
+		vscode.window.showInformationMessage('No tests found. Current file is not a test file.');
+		return;
+	}
+
+	await editor.document.save();
+	const testFunctions = (await getTestFunctions(goCtx, editor.document)) ?? [];
+	// We use functionName if it was provided as argument
+	// Otherwise find any test function containing the cursor.
+	const currentTestFunctions = testFunctions.filter((func) => func.range.contains(editor.selection.start));
+	const testFunctionName =
+		args && args.functionName ? args.functionName : currentTestFunctions.map((el) => el.name)[0];
+
+	if (!testFunctionName || currentTestFunctions.length === 0) {
+		vscode.window.showInformationMessage('No test function found at cursor.');
+		return;
+	}
+
+	let subTestName: string | undefined = args?.subTestName;
+
+	if (!subTestName) {
+		const testFunction = currentTestFunctions[0];
+		const simpleRunRegex = /t.Run\("([^"]+)",/;
+		const runRegex = /t.Run\(/;
+		let lineText: string;
+		let runMatch: RegExpMatchArray | null | undefined;
+		let simpleMatch: RegExpMatchArray | null | undefined;
+		for (let i = editor.selection.start.line; i >= testFunction.range.start.line; i--) {
+			lineText = editor.document.lineAt(i).text;
+			simpleMatch = lineText.match(simpleRunRegex);
+			runMatch = lineText.match(runRegex);
+			if (simpleMatch || (runMatch && !simpleMatch)) {
+				break;
+			}
+		}
+
+		if (!simpleMatch) {
+			const input = await vscode.window.showInputBox({
+				prompt: 'Enter sub test name'
+			});
+			if (input) {
+				subTestName = input;
+			} else {
+				vscode.window.showInformationMessage('No subtest function with a simple subtest name found at cursor.');
+				return;
+			}
+		} else {
+			subTestName = simpleMatch[1];
+		}
+	}
+
+	await editor.document.save();
+
+	const escapedName = escapeSubTestName(testFunctionName, subTestName);
+
+	if (cmd === 'debug') {
+		return debugTestAtCursor(editor, escapedName, testFunctions, goConfig);
+	} else if (cmd === 'test') {
+		return runTestAtCursor(editor, escapedName, testFunctions, goConfig, cmd, args);
+	} else {
+		throw new Error(`Unsupported command: ${cmd}`);
+	}
+}
+
 /**
  * Executes the unit test at the primary cursor using `go test`. Output
  * is sent to the 'Go' channel.
@@ -150,77 +227,23 @@
 }
 
 /**
- * Executes the sub unit test at the primary cursor using `go test`. Output
- * is sent to the 'Go' channel.
+ * Executes the sub unit test at the primary cursor.
+ *
+ * @param cmd Whether the command is test or debug.
  */
-export const subTestAtCursor: CommandFactory = (ctx, goCtx) => {
-	return async (args: any) => {
-		const goConfig = getGoConfig();
-		const editor = vscode.window.activeTextEditor;
-		if (!editor) {
-			vscode.window.showInformationMessage('No editor is active.');
-			return;
-		}
-		if (!editor.document.fileName.endsWith('_test.go')) {
-			vscode.window.showInformationMessage('No tests found. Current file is not a test file.');
-			return;
-		}
-
-		await editor.document.save();
+export function subTestAtCursor(cmd: SubTestAtCursorCmd): CommandFactory {
+	return (_, goCtx) => async (args: string[]) => {
 		try {
-			const testFunctions = (await getTestFunctions(goCtx, editor.document)) ?? [];
-			// We use functionName if it was provided as argument
-			// Otherwise find any test function containing the cursor.
-			const currentTestFunctions = testFunctions.filter((func) => func.range.contains(editor.selection.start));
-			const testFunctionName =
-				args && args.functionName ? args.functionName : currentTestFunctions.map((el) => el.name)[0];
-
-			if (!testFunctionName || currentTestFunctions.length === 0) {
-				vscode.window.showInformationMessage('No test function found at cursor.');
-				return;
-			}
-
-			const testFunction = currentTestFunctions[0];
-			const simpleRunRegex = /t.Run\("([^"]+)",/;
-			const runRegex = /t.Run\(/;
-			let lineText: string;
-			let runMatch: RegExpMatchArray | null | undefined;
-			let simpleMatch: RegExpMatchArray | null | undefined;
-			for (let i = editor.selection.start.line; i >= testFunction.range.start.line; i--) {
-				lineText = editor.document.lineAt(i).text;
-				simpleMatch = lineText.match(simpleRunRegex);
-				runMatch = lineText.match(runRegex);
-				if (simpleMatch || (runMatch && !simpleMatch)) {
-					break;
-				}
-			}
-
-			let subtest: string;
-			if (!simpleMatch) {
-				const input = await vscode.window.showInputBox({
-					prompt: 'Enter sub test name'
-				});
-				if (input) {
-					subtest = input;
-				} else {
-					vscode.window.showInformationMessage(
-						'No subtest function with a simple subtest name found at cursor.'
-					);
-					return;
-				}
-			} else {
-				subtest = simpleMatch[1];
-			}
-
-			const subTestName = testFunctionName + '/' + subtest;
-
-			return await runTestAtCursor(editor, subTestName, testFunctions, goConfig, 'test', args);
+			return await _subTestAtCursor(goCtx, getGoConfig(), cmd, args);
 		} catch (err) {
-			vscode.window.showInformationMessage('Unable to run subtest: ' + (err as any).toString());
-			console.error(err);
+			if (err instanceof NotFoundError) {
+				vscode.window.showInformationMessage(err.message);
+			} else {
+				console.error(err);
+			}
 		}
 	};
-};
+}
 
 /**
  * Debugs the test at cursor.
diff --git a/src/goTools.ts b/src/goTools.ts
index 8db1dc8..7a47263 100644
--- a/src/goTools.ts
+++ b/src/goTools.ts
@@ -188,9 +188,9 @@
 			break;
 	}
 
-	// Only add format tools if the language server is disabled and the
+	// Only add format tools if the language server is disabled or the
 	// format tool is known to us.
-	if (goConfig['useLanguageServer'] === false && !usingCustomFormatTool(goConfig)) {
+	if (goConfig['useLanguageServer'] === false || usingCustomFormatTool(goConfig)) {
 		maybeAddTool(getFormatTool(goConfig));
 	}
 
diff --git a/src/goToolsInformation.ts b/src/goToolsInformation.ts
index a0ac0bc..b2dbaf0 100644
--- a/src/goToolsInformation.ts
+++ b/src/goToolsInformation.ts
@@ -213,8 +213,8 @@
 		minimumGoVersion: semver.coerce('1.13'),
 		latestVersion: semver.parse('v0.10.1'),
 		latestVersionTimestamp: moment('2022-11-01', 'YYYY-MM-DD'),
-		latestPrereleaseVersion: semver.parse('v0.10.1'),
-		latestPrereleaseVersionTimestamp: moment('2022-11-01', 'YYYY-MM-DD')
+		latestPrereleaseVersion: semver.parse('v0.11.0-pre.3'),
+		latestPrereleaseVersionTimestamp: moment('2022-12-13', 'YYYY-MM-DD')
 	},
 	'dlv': {
 		name: 'dlv',
diff --git a/src/goVulncheck.ts b/src/goVulncheck.ts
index 87f0665..2fe4fd7 100644
--- a/src/goVulncheck.ts
+++ b/src/goVulncheck.ts
@@ -2,478 +2,373 @@
  * Copyright 2022 The Go Authors. All rights reserved.
  * Licensed under the MIT License. See LICENSE in the project root for license information.
  *--------------------------------------------------------*/
-
-import path from 'path';
-import fs from 'fs';
-import * as vscode from 'vscode';
-import { GoExtensionContext } from './context';
-import { getBinPath } from './util';
-import * as cp from 'child_process';
-import { toolExecutionEnvironment } from './goEnv';
-import { killProcessTree } from './utils/processUtils';
-import * as readline from 'readline';
+import path = require('path');
+import vscode = require('vscode');
 import { URI } from 'vscode-uri';
-import { promisify } from 'util';
-import { runGoEnv } from './goModules';
-import { ExecuteCommandParams, ExecuteCommandRequest } from 'vscode-languageserver-protocol';
+import { getGoConfig } from './config';
 
-export class VulncheckResultViewProvider implements vscode.CustomTextEditorProvider {
-	public static readonly viewType = 'vulncheck.view';
-
-	public static register(
-		{ extensionUri, subscriptions }: vscode.ExtensionContext,
-		goCtx: GoExtensionContext
-	): VulncheckResultViewProvider {
-		const provider = new VulncheckResultViewProvider(extensionUri, goCtx);
-		subscriptions.push(vscode.window.registerCustomEditorProvider(VulncheckResultViewProvider.viewType, provider));
-		return provider;
+function moduleVersion(mod: string, ver: string | undefined) {
+	if (!ver) {
+		return 'N/A';
 	}
-
-	constructor(private readonly extensionUri: vscode.Uri, private readonly goCtx: GoExtensionContext) {}
-
-	/**
-	 * Called when our custom editor is opened.
-	 */
-	public async resolveCustomTextEditor(
-		document: vscode.TextDocument,
-		webviewPanel: vscode.WebviewPanel,
-		_: vscode.CancellationToken // eslint-disable-line @typescript-eslint/no-unused-vars
-	): Promise<void> {
-		// Setup initial content for the webview
-		webviewPanel.webview.options = { enableScripts: true };
-		webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview);
-
-		// Receive message from the webview.
-		webviewPanel.webview.onDidReceiveMessage(this.handleMessage, this);
-
-		function updateWebview() {
-			webviewPanel.webview.postMessage({ type: 'update', text: document.getText() });
-		}
-
-		// Hook up event handlers so that we can synchronize the webview with the text document.
-		//
-		// The text document acts as our model, so we have to sync change in the document to our
-		// editor and sync changes in the editor back to the document.
-		//
-		// Remember that a single text document can also be shared between multiple custom
-		// editors (this happens for example when you split a custom editor)
-		const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument((e) => {
-			if (e.document.uri.toString() === document.uri.toString()) {
-				updateWebview();
-			}
-		});
-
-		// Make sure we get rid of the listener when our editor is closed.
-		webviewPanel.onDidDispose(() => {
-			changeDocumentSubscription.dispose();
-		});
-
-		updateWebview();
+	if (mod === 'stdlib') {
+		return `go${ver.replace(/^(v|go)/, '')}`;
 	}
-
-	/**
-	 * Get the static html used for the editor webviews.
-	 */
-	private getHtmlForWebview(webview: vscode.Webview): string {
-		const mediaUri = vscode.Uri.joinPath(this.extensionUri, 'media');
-		// Local path to script and css for the webview
-		const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(mediaUri, 'vulncheckView.js'));
-		const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(mediaUri, 'reset.css'));
-		const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(mediaUri, 'vscode.css'));
-		const styleMainUri = webview.asWebviewUri(vscode.Uri.joinPath(mediaUri, 'vulncheckView.css'));
-		const codiconsUri = webview.asWebviewUri(vscode.Uri.joinPath(mediaUri, 'codicon.css'));
-
-		// Use a nonce to whitelist which scripts can be run
-		const nonce = getNonce();
-
-		return /* html */ `
-			<!DOCTYPE html>
-			<html lang="en">
-			<head>
-				<meta charset="UTF-8">
-				<!--
-				Use a content security policy to only allow loading images from https or from our extension directory,
-				and only allow scripts that have a specific nonce.
-				-->
-				<!--
-					Use a content security policy to only allow loading specific resources in the webview
-				-->
-				<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
-				<meta name="viewport" content="width=device-width, initial-scale=1.0">
-				<link href="${styleResetUri}" rel="stylesheet" />
-				<link href="${styleVSCodeUri}" rel="stylesheet" />
-				<link href="${styleMainUri}" rel="stylesheet" />
-				<link href="${codiconsUri}" rel="stylesheet" />
-				<title>Vulnerability Report - govulncheck</title>
-			</head>
-			<body>
-			    Vulncheck is an experimental tool.<br>
-				Share feedback at <a href="https://go.dev/s/vsc-vulncheck-feedback">go.dev/s/vsc-vulncheck-feedback</a>.
-
-				<div class="log"></div>
-				<div class="vulns"></div>
-				<div class="unaffecting"></div>
-				<div class="debug"></div>
-				<script nonce="${nonce}" src="${scriptUri}"></script>
-			</body>
-			</html>`;
-	}
-
-	private async handleMessage(e: { type: string; target?: string; dir?: string }): Promise<void> {
-		switch (e.type) {
-			case 'open':
-				{
-					if (!e.target) return;
-					const uri = safeURIParse(e.target);
-					if (!uri || !uri.scheme) return;
-					if (uri.scheme === 'https') {
-						vscode.env.openExternal(uri);
-					} else if (uri.scheme === 'file') {
-						const line = uri.query ? Number(uri.query.split(':')[0]) : undefined;
-						const range = line ? new vscode.Range(line, 0, line, 0) : undefined;
-						vscode.window.showTextDocument(
-							vscode.Uri.from({ scheme: uri.scheme, path: uri.path }),
-							// prefer the first column to present the source.
-							{ viewColumn: vscode.ViewColumn.One, selection: range }
-						);
-					}
-				}
-				return;
-			case 'fix':
-				{
-					if (!e.target || !e.dir) return;
-					const modFile = await getGoModFile(vscode.Uri.file(e.dir));
-					if (modFile) {
-						await goplsUpgradeDependency(this.goCtx, vscode.Uri.file(modFile), [e.target], false);
-						// TODO: run go mod tidy?
-					}
-				}
-				return;
-			case 'snapshot-result':
-				// response for `snapshot-request`.
-				return;
-			default:
-				console.log(`unrecognized type message: ${e.type}`);
-		}
-	}
+	return `${mod}@${ver}`;
 }
 
-const GOPLS_UPGRADE_DEPENDENCY = 'gopls.upgrade_dependency';
-async function goplsUpgradeDependency(
-	goCtx: GoExtensionContext,
-	goModFileUri: vscode.Uri,
-	goCmdArgs: string[],
-	addRequire: boolean
-): Promise<void> {
-	const { languageClient } = goCtx;
-	const uri = languageClient?.code2ProtocolConverter.asUri(goModFileUri);
-	const params: ExecuteCommandParams = {
-		command: GOPLS_UPGRADE_DEPENDENCY,
-		arguments: [
-			{
-				URI: uri,
-				GoCmdArgs: goCmdArgs,
-				AddRequire: addRequire
-			}
-		]
-	};
-	return await languageClient?.sendRequest(ExecuteCommandRequest.type, params);
-}
+// writeVulns generates human-readable vulnerability report from the VulncheckReport
+// and write to the outputChannel.
+export function writeVulns(
+	res: VulncheckReport | undefined | null,
+	outputChannel: { appendLine(value: string): void }
+) {
+	outputChannel.appendLine('');
 
-async function getGoModFile(dir: vscode.Uri): Promise<string | undefined> {
-	try {
-		const p = await runGoEnv(dir, ['GOMOD']);
-		return p['GOMOD'] === '/dev/null' || p['GOMOD'] === 'NUL' ? '' : p['GOMOD'];
-	} catch (e) {
-		vscode.window.showErrorMessage(`Failed to find 'go.mod' for ${dir}: ${e}`);
-	}
-	return;
-}
-
-export class VulncheckProvider {
-	static scheme = 'govulncheck';
-	static setup({ subscriptions }: vscode.ExtensionContext, goCtx: GoExtensionContext) {
-		const channel = vscode.window.createOutputChannel('govulncheck');
-		const instance = new this(channel);
-		subscriptions.push(
-			vscode.commands.registerCommand('go.vulncheck.run', async () => {
-				instance.run(goCtx);
-			})
-		);
-		return instance;
-	}
-
-	constructor(private channel: vscode.OutputChannel) {}
-
-	private running = false;
-
-	async run(goCtx: GoExtensionContext) {
-		if (this.running) {
-			vscode.window.showWarningMessage('another vulncheck is in progress');
-			return;
-		}
-		try {
-			this.running = true;
-			await this.runInternal(goCtx);
-		} finally {
-			this.running = false;
-		}
-	}
-
-	private async runInternal(goCtx: GoExtensionContext) {
-		const pick = await vscode.window.showQuickPick(['Current Package', 'Current Module', 'Workspace']);
-		let dir, pattern: string;
-		const document = vscode.window.activeTextEditor?.document;
-		switch (pick) {
-			case 'Current Package':
-				if (!document) {
-					vscode.window.showErrorMessage('vulncheck error: no current package');
-					return;
-				}
-				if (document.languageId !== 'go') {
-					vscode.window.showErrorMessage(
-						'File in the active editor is not a Go file, cannot find current package to check.'
-					);
-					return;
-				}
-				dir = path.dirname(document.fileName);
-				pattern = '.';
-				break;
-			case 'Current Module':
-				dir = await moduleDir(document);
-				if (!dir) {
-					vscode.window.showErrorMessage('vulncheck error: no current module');
-					return;
-				}
-				pattern = './...';
-				break;
-			case 'Workspace':
-				dir = await this.activeDir();
-				pattern = './...';
-				break;
-			default:
-				return;
-		}
-		if (!dir) {
-			return;
-		}
-
-		this.channel.clear();
-		this.channel.show();
-		this.channel.appendLine(`cd ${dir}; gopls vulncheck ${pattern}`);
-
-		try {
-			const start = new Date();
-			const vuln = await vulncheck(goCtx, dir, pattern, this.channel);
-
-			if (vuln?.Vuln?.length) {
-				fillAffectedPkgs(vuln.Vuln);
-
-				// record run info.
-				vuln.Start = start;
-				vuln.Duration = Date.now() - start.getTime();
-				vuln.Dir = dir;
-				vuln.Pattern = pattern;
-
-				// write to file and visualize it!
-				const fname = path.join(dir, `vulncheck-${Date.now()}.vulncheck.json`);
-				const writeFile = promisify(fs.writeFile);
-				await writeFile(fname, JSON.stringify(vuln));
-				const uri = URI.file(fname);
-				const viewColumn = vscode.ViewColumn.Beside;
-				vscode.commands.executeCommand(
-					'vscode.openWith',
-					uri,
-					VulncheckResultViewProvider.viewType,
-					viewColumn
-				);
-				this.channel.appendLine(`Vulncheck - result written in ${fname}`);
-			} else {
-				this.channel.appendLine('Vulncheck - found no vulnerability');
-			}
-		} catch (e) {
-			vscode.window.showErrorMessage(`error running vulncheck: ${e}`);
-			this.channel.appendLine(`Vulncheck failed: ${e}`);
-		}
-		this.channel.show();
-	}
-
-	private async activeDir() {
-		const folders = vscode.workspace.workspaceFolders;
-		if (!folders || folders.length === 0) return;
-		let dir: string | undefined = '';
-		if (folders.length === 1) {
-			dir = folders[0].uri.path;
-		} else {
-			const pick = await vscode.window.showQuickPick(
-				folders.map((f) => ({ label: f.name, description: f.uri.path }))
-			);
-			dir = pick?.description;
-		}
-		return dir;
-	}
-}
-
-async function moduleDir(document: vscode.TextDocument | undefined) {
-	const docDir = document && document.fileName && path.dirname(document.fileName);
-	if (!docDir) {
+	if (!res) {
+		outputChannel.appendLine('Error - invalid vulncheck result.'); // TODO(hyangah): ask to open an issue.
 		return;
 	}
-	const modFile = await getGoModFile(vscode.Uri.file(docDir));
-	if (!modFile) {
+	if (!res.Vulns || res.Vulns.length === 0) {
+		outputChannel.appendLine('No vulnerability found.');
 		return;
 	}
-	return path.dirname(modFile);
-}
 
-// run `gopls vulncheck`.
-export async function vulncheck(
-	goCtx: GoExtensionContext,
-	dir: string,
-	pattern = './...',
-	channel: { appendLine: (msg: string) => void }
-): Promise<VulncheckReport> {
-	const { languageClient, serverInfo } = goCtx;
-	const COMMAND = 'gopls.run_vulncheck_exp';
-	if (!languageClient || !serverInfo?.Commands?.includes(COMMAND)) {
-		throw Promise.reject('this feature requires gopls v0.8.4 or newer');
-	}
-	// TODO: read back the actual package configuration from gopls.
-	const gopls = getBinPath('gopls');
-	const options: vscode.ProgressOptions = {
-		cancellable: true,
-		title: 'Run govulncheck',
-		location: vscode.ProgressLocation.Notification
-	};
-	const task = vscode.window.withProgress<VulncheckReport>(options, (progress, token) => {
-		const p = cp.spawn(gopls, ['vulncheck', pattern], {
-			cwd: dir,
-			env: toolExecutionEnvironment(vscode.Uri.file(dir))
-		});
-
-		progress.report({ message: `starting command ${gopls} from ${dir}  (pid; ${p.pid})` });
-
-		const d = token.onCancellationRequested(() => {
-			channel.appendLine(`gopls vulncheck (pid: ${p.pid}) is cancelled`);
-			killProcessTree(p);
-			d.dispose();
-		});
-
-		const promise = new Promise<VulncheckReport>((resolve, reject) => {
-			const rl = readline.createInterface({ input: p.stderr });
-			rl.on('line', (line) => {
-				channel.appendLine(line);
-				const msg = line.match(/^\d+\/\d+\/\d+\s+\d+:\d+:\d+\s+(.*)/);
-				if (msg && msg[1]) {
-					progress.report({ message: msg[1] });
-				}
-			});
-
-			let buf = '';
-			p.stdout.on('data', (chunk) => {
-				buf += chunk;
-			});
-			p.stdout.on('close', () => {
-				try {
-					const res: VulncheckReport = JSON.parse(buf);
-					resolve(res);
-				} catch (e) {
-					if (token.isCancellationRequested) {
-						reject('analysis cancelled');
-					} else {
-						channel.appendLine(buf);
-						reject('vulncheck failed: see govulncheck OUTPUT');
-					}
-				}
+	const affecting = res.Vulns.filter((v) => {
+		return v.Modules?.some((m) => {
+			return m.Packages?.some((p) => {
+				return p.CallStacks?.some((cs) => {
+					return cs.Frames && cs.Frames.length > 0;
+				});
 			});
 		});
-		return promise;
 	});
-	return await task;
+	const unaffecting = res.Vulns.filter((v) => !affecting.includes(v));
+
+	switch (affecting.length) {
+		case 0:
+			outputChannel.appendLine('No vulnerability found.');
+			break;
+		case 1:
+			outputChannel.appendLine(`Found ${affecting.length} affecting vulnerability.`);
+			outputChannel.appendLine('-'.repeat(80));
+			break;
+		default:
+			outputChannel.appendLine(`Found ${affecting.length} affecting vulnerabilities.`);
+			outputChannel.appendLine('-'.repeat(80));
+			break;
+	}
+
+	affecting.forEach((vuln) => {
+		outputChannel.appendLine(`⚠ ${vuln.OSV.id} (https://pkg.go.dev/vuln/${vuln.OSV.id})`);
+		const desc = (vuln.OSV.details || '').trimRight();
+		const aliases = vuln.OSV.aliases?.length ? ` (${vuln.OSV.aliases.join(', ')})` : '';
+		outputChannel.appendLine(`\n${desc}${aliases}\n`);
+		vuln.Modules?.forEach((mod) => {
+			outputChannel.appendLine(`Found Version: ${moduleVersion(mod.Path, mod.FoundVersion)}`);
+			outputChannel.appendLine(`Fixed Version: ${moduleVersion(mod.Path, mod.FixedVersion)}`);
+			mod.Packages?.forEach((pkg) => {
+				outputChannel.appendLine('\nCall stacks in your code:');
+				pkg.CallStacks?.forEach((cs, index) => {
+					// TODO: the position info embedded in the cs.Summary is relative to
+					// the directory gopls ran the vulnchek.
+					// Instead replace with workspace-relative paths.
+					outputChannel.appendLine(`- ${cs.Summary}`);
+					// Print the first trace (index === 0) as an example.
+					// TODO(hyangah): allow users to see example traces for all detected vulnerable symbols.
+					if (index === 0 && cs.Frames) {
+						const last = cs.Frames.length - 1;
+						cs.Frames?.forEach((f, index) => {
+							// Skip the last frame that just carries the vulnerable symbol.
+							// This info is already included in cs.Summary.
+							if (last === index) return;
+							const line = f.Position?.Line || 1;
+							// TODO: shorten f.Position.Filename (e.g. workspace relative path, and home directory ~ relative path)
+							const pos = f.Position?.Filename ? `${f.Position.Filename}:${line}` : ' - ';
+							const name = f.RecvType ? `${f.RecvType}.${f.FuncName}` : `${f.PkgPath}.${f.FuncName}`;
+							outputChannel.appendLine(`\t${name}\n\t\t(${pos})`);
+						});
+					}
+				});
+			});
+		});
+		outputChannel.appendLine('-'.repeat(80));
+	});
+
+	if (unaffecting.length) {
+		outputChannel.appendLine(`
+# The vulnerabilities below are in packages that you import, but your code does
+# not appear to call any vulnerable functions. You may not need to take any
+# action. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck for details.
+`);
+
+		switch (unaffecting.length) {
+			case 1:
+				outputChannel.appendLine(`Found ${unaffecting.length} unused vulnerability.`);
+				break;
+			default:
+				outputChannel.appendLine(`Found ${unaffecting.length} unused vulnerabilities.`);
+				break;
+		}
+		outputChannel.appendLine('-'.repeat(80));
+	}
+
+	unaffecting.forEach((vuln) => {
+		outputChannel.appendLine(`ⓘ ${vuln.OSV.id} (https://pkg.go.dev/vuln/${vuln.OSV.id})`);
+		const desc = (vuln.OSV.details || '').trimRight();
+		const aliases = vuln.OSV.aliases?.length ? ` (${vuln.OSV.aliases.join(', ')})` : '';
+		outputChannel.appendLine(`\n${desc}${aliases}\n`);
+		vuln.Modules?.forEach((mod) => {
+			outputChannel.appendLine(`Found Version: ${moduleVersion(mod.Path, mod.FoundVersion)}`);
+			outputChannel.appendLine(`Fixed Version: ${moduleVersion(mod.Path, mod.FixedVersion)}`);
+			mod.Packages?.forEach((pkg) => {
+				outputChannel.appendLine(`Package: ${pkg.Path}`);
+			});
+		});
+		outputChannel.appendLine('-'.repeat(80));
+	});
 }
 
-interface VulncheckReport {
+// VulncheckReport is the JSON data type of gopls's vulncheck result.
+export interface VulncheckReport {
 	// Vulns populated by gopls vulncheck run.
-	Vuln?: Vuln[];
+	Vulns?: Vuln[];
 
-	// analysis run information.
-	Pattern?: string;
-	Dir?: string;
-
-	Start?: Date;
-	Duration?: number; // milliseconds
+	Mode?: 'govulncheck' | 'imports';
 }
 
+// Vuln represents a single OSV entry.
 interface Vuln {
-	ID: string;
-	Details: string;
-	Aliases: string[];
-	Symbol: string;
-	PkgPath: string;
-	ModPath: string;
-	URL: string;
-	CurrentVersion: string;
-	FixedVersion: string;
-	CallStacks?: CallStack[][];
-	CallStacksSummary?: string[];
+	// OSV contains all data from the OSV entry for this vulnerability.
+	OSV: OSVEntry;
 
-	// Derived from call stacks.
-	// TODO(hyangah): add to gopls vulncheck.
-	AffectedPkgs?: string[];
+	// Modules contains all of the modules in the OSV entry where a
+	// vulnerable package is imported by the target source code or binary.
+	//
+	// For example, a module M with two packages M/p1 and M/p2, where only p1
+	// is vulnerable, will appear in this list if and only if p1 is imported by
+	// the target source code or binary.
+	Modules: Module[];
+
+	AffectedPackages?: string[];
+}
+
+interface OSVEntry {
+	id: string;
+	published?: string;
+	aliases?: string[];
+	details?: string;
+	affected?: Affected[];
+}
+
+interface Affected {
+	package: Package;
+	ecosystem_specific?: EcosystemSpecific;
+}
+
+interface EcosystemSpecificImport {
+	path: string;
+	goos?: string[];
+	goarch?: string[];
+	symbols?: string[];
+}
+
+interface EcosystemSpecific {
+	imports?: EcosystemSpecificImport[];
+}
+
+interface Package {
+	name: string;
+}
+
+interface Module {
+	// Path is the module path of the module containing the vulnerability.
+	//
+	// Importable packages in the standard library will have the path "stdlib".
+	Path: string;
+
+	// FoundVersion is the module version where the vulnerability was found.
+	FoundVersion?: string;
+
+	// FixedVersion is the module version where the vulnerability was
+	// fixed. If there are multiple fixed versions in the OSV report, this will
+	// be the latest fixed version.
+	//
+	// This is empty if a fix is not available.
+	FixedVersion?: string;
+
+	// Packages contains all the vulnerable packages in OSV entry that are
+	// imported by the target source code or binary.
+	//
+	// For example, given a module M with two packages M/p1 and M/p2, where
+	// both p1 and p2 are vulnerable, p1 and p2 will each only appear in this
+	// list they are individually imported by the target source code or binary.
+	Packages?: Package[];
+}
+
+interface Package {
+	// Path is the import path of the package containing the vulnerability.
+	Path: string;
+
+	// CallStacks contains a representative call stack for each
+	// vulnerable symbol that is called.
+	//
+	// For vulnerabilities found from binary analysis, only CallStack.Symbol
+	// will be provided.
+	//
+	// For non-affecting vulnerabilities reported from the source mode
+	// analysis, this will be empty.
+	CallStacks?: CallStack[];
 }
 
 interface CallStack {
-	Name: string;
-	URI: string;
-	Pos: {
-		line: number;
-		character: number;
-	};
+	// Symbol is the name of the detected vulnerable function
+	// or method.
+	//
+	// This follows the naming convention in the OSV report.
+	Symbol?: string;
+
+	// Summary is a one-line description of the callstack, used by the
+	// default govulncheck mode.
+	//
+	// Example: module3.main calls github.com/shiyanhui/dht.DHT.Run
+	Summary?: string;
+
+	// Frames contains an entry for each stack in the call stack.
+	//
+	// Frames are sorted starting from the entry point to the
+	// imported vulnerable symbol. The last frame in Frames should match
+	// Symbol.
+	Frames?: StackFrame[];
 }
 
-function getNonce() {
-	let text = '';
-	const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
-	for (let i = 0; i < 32; i++) {
-		text += possible.charAt(Math.floor(Math.random() * possible.length));
+interface StackFrame {
+	// PackagePath is the import path.
+	PkgPath: string;
+
+	// FuncName is the function name.
+	FuncName?: string;
+
+	// RecvType is the fully qualified receiver type,
+	// if the called symbol is a method.
+	//
+	// The client can create the final symbol name by
+	// prepending RecvType to FuncName.
+	RecvType?: string;
+
+	// Position describes an arbitrary source position
+	// including the file, line, and column location.
+	// A Position is valid if the line number is > 0.
+	Position?: Position;
+}
+
+interface Position {
+	Filename?: string; // filename, if any
+	Offset?: number; // offset, starting at 0
+	Line?: number; // line number, starting at 1
+	Column?: number; // column number, starting at 1 (byte count)
+}
+
+// VulncheckOutputLinkProvider linkifies govulncheck output.
+export class VulncheckOutputLinkProvider implements vscode.DocumentLinkProvider {
+	static activate(ctx: Pick<vscode.ExtensionContext, 'subscriptions'>) {
+		ctx.subscriptions.push(
+			vscode.languages.registerDocumentLinkProvider(
+				{ language: 'govulncheck' },
+				new VulncheckOutputLinkProvider()
+			)
+		);
 	}
-	return text;
-}
 
-function safeURIParse(s: string): URI | undefined {
-	try {
-		return URI.parse(s);
-	} catch (_) {
-		return undefined;
+	provideDocumentLinks(
+		document: vscode.TextDocument,
+		// eslint-disable-next-line @typescript-eslint/no-unused-vars
+		_token: vscode.CancellationToken
+	): vscode.ProviderResult<vscode.DocumentLink[]> {
+		try {
+			return this.unsafeProvideDocumentLinks(document);
+		} catch (e) {
+			console.log(`failed to linkify govulncheck output result: ${e}`);
+		}
+		return [];
+	}
+
+	unsafeProvideDocumentLinks(document: vscode.TextDocument): vscode.ProviderResult<vscode.DocumentLink[]> {
+		const ret = [] as vscode.DocumentLink[];
+		let cwd = '';
+		for (let i = 0; i < document.lineCount; i++) {
+			const readLine = document.lineAt(i);
+
+			// govulncheck ./... for file:///foo/go.mod.
+			const cmdPattern = readLine.text.match(/^govulncheck\s+\S+\s+for\s+(file:.*\.mod)/);
+			if (cmdPattern && cmdPattern[1]) {
+				cwd = path.dirname(vscode.Uri.parse(cmdPattern[1]).fsPath);
+				continue;
+			}
+
+			// Found Version: and Fixed Version:
+			const foundOrFixedVersionPattern = readLine.text.match(/^(?:Found|Fixed) Version:\s+(\S+@\S+)$/);
+			if (foundOrFixedVersionPattern && foundOrFixedVersionPattern[1]) {
+				const modVersion = foundOrFixedVersionPattern[1];
+				const start = readLine.text.indexOf(modVersion);
+				const end = start + modVersion.length;
+				const link = new vscode.DocumentLink(
+					new vscode.Range(i, start, i, end),
+					vscode.Uri.parse(`https://pkg.go.dev/${modVersion}`)
+				);
+				link.tooltip = `https://pkg.go.dev/${modVersion}`;
+				ret.push(link);
+				continue;
+			}
+
+			// Position at file (e.g. file.go:1:2)
+			const filePosPattern = readLine.text.match(/(?:-\s+|\s+\()(\S+\.go):(\d+)(?::(\d+)){0,1}/);
+			if (filePosPattern && filePosPattern[1]) {
+				let fname = filePosPattern[1];
+				if (!path.isAbsolute(fname)) {
+					fname = path.join(cwd, fname);
+				}
+				if (path.isAbsolute(fname)) {
+					const line = filePosPattern[2];
+					const col = filePosPattern[3];
+					const fragment = col ? { fragment: `L${line},${col}` } : { fragment: `L${line}` };
+					const uri = URI.file(fname).with(fragment);
+					const start = readLine.text.indexOf(filePosPattern[1]);
+					const end = readLine.text.indexOf(filePosPattern[0]) + filePosPattern[0].length;
+					const link = new vscode.DocumentLink(new vscode.Range(i, start, i, end), uri);
+					ret.push(link);
+				}
+				continue;
+			}
+		}
+		return ret;
 	}
 }
 
-// Computes the AffectedPkgs attribute if it's not present.
-// Exported for testing.
-// TODO(hyangah): move this logic to gopls vulncheck or govulncheck.
-export function fillAffectedPkgs(vulns: Vuln[] | undefined): Vuln[] {
-	if (!vulns) return [];
+export const toggleVulncheckCommandFactory = () => () => {
+	const editor = vscode.window.activeTextEditor;
+	const documentUri = editor?.document.uri;
+	toggleVulncheckCommand(documentUri);
+};
 
-	const re = new RegExp(/^(\S+)\/([^/\s]+)$/);
-	vulns.forEach((vuln) => {
-		// If it's already set by gopls vulncheck, great!
-		if (vuln.AffectedPkgs) return;
-
-		const affected = new Set<string>();
-		vuln.CallStacks?.forEach((cs) => {
-			if (!cs || cs.length === 0) {
-				return;
-			}
-			const name = cs[0].Name || '';
-			const m = name.match(re);
-			if (!m) {
-				name && affected.add(name);
-			} else {
-				const pkg = m[2] && m[2].split('.')[0];
-				affected.add(`${m[1]}/${pkg}`);
-			}
-		});
-		vuln.AffectedPkgs = Array.from(affected);
-	});
-	return vulns;
+function toggleVulncheckCommand(uri?: URI) {
+	const goCfgName = 'diagnostic.vulncheck';
+	const cfg = getGoConfig(uri);
+	const { globalValue, workspaceValue, workspaceFolderValue } = cfg.inspect(goCfgName) || {};
+	if (workspaceFolderValue) {
+		const newValue = workspaceFolderValue === 'Imports' ? 'Off' : 'Imports';
+		cfg.update(goCfgName, newValue);
+		return;
+	}
+	if (workspaceValue) {
+		const newValue = workspaceValue === 'Imports' ? 'Off' : 'Imports';
+		cfg.update(goCfgName, newValue, false);
+		return;
+	}
+	if (globalValue) {
+		const newValue = globalValue === 'Imports' ? 'Off' : 'Imports';
+		cfg.update(goCfgName, newValue, true);
+		return;
+	}
+	cfg.update(goCfgName, 'Imports');
 }
diff --git a/src/language/goLanguageServer.ts b/src/language/goLanguageServer.ts
index 69f389f..30fee57 100644
--- a/src/language/goLanguageServer.ts
+++ b/src/language/goLanguageServer.ts
@@ -20,12 +20,15 @@
 	ConfigurationParams,
 	ConfigurationRequest,
 	ErrorAction,
+	ExecuteCommandParams,
+	ExecuteCommandRequest,
 	ExecuteCommandSignature,
 	HandleDiagnosticsSignature,
 	InitializeError,
 	InitializeResult,
 	LanguageClientOptions,
 	Message,
+	ProgressToken,
 	ProvideCodeLensesSignature,
 	ProvideCompletionItemsSignature,
 	ProvideDocumentFormattingEditsSignature,
@@ -56,6 +59,8 @@
 import { maybePromptForDeveloperSurvey } from '../goDeveloperSurvey';
 import { CommandFactory } from '../commands';
 import { updateLanguageServerIconGoStatusBar } from '../goStatus';
+import { URI } from 'vscode-uri';
+import { VulncheckReport, writeVulns } from '../goVulncheck';
 
 export interface LanguageServerConfig {
 	serverName: string;
@@ -333,6 +338,9 @@
 		if (!goCtx.serverTraceChannel) {
 			goCtx.serverTraceChannel = vscode.window.createOutputChannel(cfg.serverName);
 		}
+		if (!goCtx.govulncheckOutputChannel) {
+			goCtx.govulncheckOutputChannel = vscode.window.createOutputChannel('govulncheck', 'govulncheck');
+		}
 	}
 	return Object.assign(
 		{
@@ -343,12 +351,35 @@
 	);
 }
 
+export class GoLanguageClient extends LanguageClient implements vscode.Disposable {
+	constructor(
+		id: string,
+		name: string,
+		serverOptions: ServerOptions,
+		clientOptions: LanguageClientOptions,
+		private onDidChangeVulncheckResultEmitter: vscode.EventEmitter<VulncheckEvent>
+	) {
+		super(id, name, serverOptions, clientOptions);
+	}
+
+	dispose() {
+		this.onDidChangeVulncheckResultEmitter.dispose();
+	}
+	public get onDidChangeVulncheckResult(): vscode.Event<VulncheckEvent> {
+		return this.onDidChangeVulncheckResultEmitter.event;
+	}
+}
+
+type VulncheckEvent = {
+	URI?: URI;
+};
+
 // buildLanguageClient returns a language client built using the given language server config.
 // The returned language client need to be started before use.
 export async function buildLanguageClient(
 	goCtx: GoExtensionContext,
 	cfg: BuildLanguageClientOption
-): Promise<LanguageClient> {
+): Promise<GoLanguageClient> {
 	const goplsWorkspaceConfig = await adjustGoplsWorkspaceConfiguration(cfg, getGoplsConfig(), 'gopls', undefined);
 
 	const documentSelector = [
@@ -365,7 +396,11 @@
 	// in initializationFailedHandler and handle it in the connectionCloseHandler.
 	let initializationError: WebRequest.ResponseError<InitializeError> | undefined = undefined;
 
-	const c = new LanguageClient(
+	const govulncheckOutputChannel = goCtx.govulncheckOutputChannel;
+	const pendingVulncheckProgressToken = new Map<ProgressToken, any>();
+	const onDidChangeVulncheckResultEmitter = new vscode.EventEmitter<VulncheckEvent>();
+
+	const c = new GoLanguageClient(
 		'go', // id
 		cfg.serverName, // name e.g. gopls
 		{
@@ -439,10 +474,52 @@
 				}
 			},
 			middleware: {
+				handleWorkDoneProgress: async (token, params, next) => {
+					switch (params.kind) {
+						case 'begin':
+							break;
+						case 'report':
+							if (pendingVulncheckProgressToken.has(token) && params.message) {
+								govulncheckOutputChannel?.appendLine(params.message);
+							}
+							break;
+						case 'end':
+							if (pendingVulncheckProgressToken.has(token)) {
+								const out = pendingVulncheckProgressToken.get(token);
+								pendingVulncheckProgressToken.delete(token);
+								if (params.message === 'completed') {
+									// success. In case of failure, it will be 'failed'
+									onDidChangeVulncheckResultEmitter.fire({ URI: out.URI });
+								}
+							}
+					}
+					next(token, params);
+				},
 				executeCommand: async (command: string, args: any[], next: ExecuteCommandSignature) => {
 					try {
-						return await next(command, args);
+						if (command === 'gopls.run_govulncheck' && args.length) {
+							await vscode.workspace.saveAll(false);
+							// TODO: move this output printing to goVulncheck.ts.
+							govulncheckOutputChannel?.replace(`govulncheck ./... for ${args[0].URI}\n`);
+							govulncheckOutputChannel?.appendLine('govulncheck is an experimental tool.');
+							govulncheckOutputChannel?.appendLine(
+								'Share feedback at https://go.dev/s/vsc-vulncheck-feedback.\n'
+							);
+							govulncheckOutputChannel?.show();
+						}
+						if (command === 'gopls.tidy') {
+							await vscode.workspace.saveAll(false);
+						}
+						const res = await next(command, args);
+						if (command === 'gopls.run_govulncheck') {
+							const progressToken = res.Token;
+							if (progressToken) {
+								pendingVulncheckProgressToken.set(progressToken, args[0]);
+							}
+						}
+						return res;
 					} catch (e) {
+						// TODO: how to print ${e} reliably???
 						const answer = await vscode.window.showErrorMessage(
 							`Command '${command}' failed: ${e}.`,
 							'Show Trace'
@@ -621,8 +698,23 @@
 					}
 				}
 			}
-		} as LanguageClientOptions
+		} as LanguageClientOptions,
+		onDidChangeVulncheckResultEmitter
 	);
+	onDidChangeVulncheckResultEmitter.event(async (e: VulncheckEvent) => {
+		if (!govulncheckOutputChannel) return;
+		if (!e || !e.URI) {
+			govulncheckOutputChannel.appendLine(`unexpected vulncheck event: ${JSON.stringify(e)}`);
+			return;
+		}
+
+		try {
+			const res = await goplsFetchVulncheckResult(goCtx, e.URI.toString());
+			writeVulns(res, govulncheckOutputChannel);
+		} catch (e) {
+			govulncheckOutputChannel.appendLine(`Fetching govulncheck output from gopls failed ${e}`);
+		}
+	});
 	return c;
 }
 
@@ -704,8 +796,10 @@
 
 	workspaceConfig = filterGoplsDefaultConfigValues(workspaceConfig, resource);
 	// note: workspaceConfig is a modifiable, valid object.
-	workspaceConfig = passGoConfigToGoplsConfigValues(workspaceConfig, getGoConfig(resource));
-	workspaceConfig = await passInlayHintConfigToGopls(cfg, workspaceConfig, getGoConfig(resource));
+	const goConfig = getGoConfig(resource);
+	workspaceConfig = passGoConfigToGoplsConfigValues(workspaceConfig, goConfig);
+	workspaceConfig = await passInlayHintConfigToGopls(cfg, workspaceConfig, goConfig);
+	workspaceConfig = await passVulncheckConfigToGopls(cfg, workspaceConfig, goConfig);
 
 	// Only modify the user's configurations for the Nightly.
 	if (!extensionInfo.isPreview) {
@@ -719,7 +813,7 @@
 
 async function passInlayHintConfigToGopls(cfg: LanguageServerConfig, goplsConfig: any, goConfig: any) {
 	const goplsVersion = await getLocalGoplsVersion(cfg);
-	if (!goplsVersion) return;
+	if (!goplsVersion) return goplsConfig ?? {};
 	const version = semver.parse(goplsVersion.version);
 	if ((version?.compare('0.8.4') ?? 1) > 0) {
 		const { inlayHints } = goConfig;
@@ -730,6 +824,19 @@
 	return goplsConfig;
 }
 
+async function passVulncheckConfigToGopls(cfg: LanguageServerConfig, goplsConfig: any, goConfig: any) {
+	const goplsVersion = await getLocalGoplsVersion(cfg);
+	if (!goplsVersion) return goplsConfig ?? {};
+	const version = semver.parse(goplsVersion.version);
+	if ((version?.compare('0.10.1') ?? 1) > 0) {
+		const vulncheck = goConfig.get('diagnostic.vulncheck');
+		if (vulncheck) {
+			goplsConfig['ui.vulncheck'] = vulncheck;
+		}
+	}
+	return goplsConfig;
+}
+
 // createTestCodeLens adds the go.test.cursor and go.debug.cursor code lens
 function createTestCodeLens(lens: vscode.CodeLens): vscode.CodeLens[] {
 	// CodeLens argument signature in gopls is [fileName: string, testFunctions: string[], benchFunctions: string[]],
@@ -1474,3 +1581,14 @@
 	const useLanguageServer = cfg.inspect<boolean>('useLanguageServer');
 	return useLanguageServer?.globalValue === undefined && useLanguageServer?.workspaceValue === undefined;
 }
+
+const GOPLS_FETCH_VULNCHECK_RESULT = 'gopls.fetch_vulncheck_result';
+async function goplsFetchVulncheckResult(goCtx: GoExtensionContext, uri: string): Promise<VulncheckReport> {
+	const { languageClient } = goCtx;
+	const params: ExecuteCommandParams = {
+		command: GOPLS_FETCH_VULNCHECK_RESULT,
+		arguments: [{ URI: uri }]
+	};
+	const res = await languageClient?.sendRequest(ExecuteCommandRequest.type, params);
+	return res[uri];
+}
diff --git a/src/language/legacy/goFormat.ts b/src/language/legacy/goFormat.ts
index a67b130..8d8164d 100644
--- a/src/language/legacy/goFormat.ts
+++ b/src/language/legacy/goFormat.ts
@@ -39,7 +39,7 @@
 		// Handle issues:
 		//  https://github.com/Microsoft/vscode-go/issues/613
 		//  https://github.com/Microsoft/vscode-go/issues/630
-		if (formatTool === 'goimports' || formatTool === 'goreturns' || formatTool === 'gofumports') {
+		if (formatTool === 'goimports' || formatTool === 'goreturns') {
 			formatFlags.push('-srcdir', filename);
 		}
 
@@ -56,8 +56,12 @@
 					return Promise.resolve([]);
 				}
 				if (err) {
+					// TODO(hyangah): investigate why this console.log is not visible at all in dev console.
+					// Ideally, this error message should be accessible through one of the output channels.
 					console.log(err);
-					return Promise.reject('Check the console in dev tools to find errors when formatting.');
+					return Promise.reject(
+						`Check the console in dev tools to find errors when formatting with ${formatTool}`
+					);
 				}
 			}
 		);
@@ -70,13 +74,12 @@
 		token: vscode.CancellationToken
 	): Thenable<vscode.TextEdit[]> {
 		const formatCommandBinPath = getBinPath(formatTool);
-
+		if (!path.isAbsolute(formatCommandBinPath)) {
+			// executable not found.
+			promptForMissingTool(formatTool);
+			return Promise.reject('failed to find tool ' + formatTool);
+		}
 		return new Promise<vscode.TextEdit[]>((resolve, reject) => {
-			if (!path.isAbsolute(formatCommandBinPath)) {
-				promptForMissingTool(formatTool);
-				return reject();
-			}
-
 			const env = toolExecutionEnvironment();
 			const cwd = path.dirname(document.fileName);
 			let stdout = '';
@@ -91,7 +94,7 @@
 			p.on('error', (err) => {
 				if (err && (<any>err).code === 'ENOENT') {
 					promptForMissingTool(formatTool);
-					return reject();
+					return reject(`failed to find format tool: ${formatTool}`);
 				}
 			});
 			p.on('close', (code) => {
@@ -136,8 +139,12 @@
 }
 
 export function getFormatTool(goConfig: { [key: string]: any }): string {
-	if (goConfig['formatTool'] === 'default') {
+	const formatTool = goConfig['formatTool'];
+	if (formatTool === 'default') {
 		return 'goimports';
 	}
-	return goConfig['formatTool'];
+	if (formatTool === 'custom') {
+		return goConfig['alternateTools']['customFormatter'] || 'goimports';
+	}
+	return formatTool;
 }
diff --git a/src/stateUtils.ts b/src/stateUtils.ts
index 4dc6791..97119fe 100644
--- a/src/stateUtils.ts
+++ b/src/stateUtils.ts
@@ -67,13 +67,10 @@
 		return [];
 	}
 	// tslint:disable-next-line: no-empty
-	if ((state as any)._value) {
-		const keys = Object.keys((state as any)._value);
-		// Filter out keys with undefined values, so they are not shown
-		// in the quick pick menu.
-		return keys.filter((key) => state.get(key) !== undefined);
-	}
-	return [];
+	const keys = state.keys();
+	// Filter out keys with undefined values, so they are not shown
+	// in the quick pick menu.
+	return keys.filter((key) => state.get(key) !== undefined);
 }
 
 async function resetStateQuickPick(state: vscode.Memento, updateFn: (key: string, value: any) => Thenable<void>) {
diff --git a/src/subTestUtils.ts b/src/subTestUtils.ts
new file mode 100644
index 0000000..76d8748
--- /dev/null
+++ b/src/subTestUtils.ts
@@ -0,0 +1,23 @@
+/*---------------------------------------------------------
+ * Copyright 2022 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+/**
+ * Escapes the subtest target name for the given test and subtest names.
+ *
+ * This function generates a name that matches a specific test by following Go
+ * regexp rules for `go test -run` argument. Specifically it escapes slashes,
+ * replaces whitespaces with underscores and wraps everything else with literal
+ * escape sequences.
+ *
+ * @param testFuncName Name of the parent test function, e.g. "TestTask"
+ * @param subTestName Name of the subtest, e.g. "GET /path/:id"
+ */
+export function escapeSubTestName(testFuncName: string, subTestName: string): string {
+	return `${testFuncName}/${subTestName}`
+		.replace(/\s/g, '_')
+		.split('/')
+		.map((part) => `\\Q${part}\\E`, '')
+		.join('$/^');
+}
diff --git a/src/utils/randomDayutils.ts b/src/utils/randomDayutils.ts
new file mode 100644
index 0000000..a1ac83f
--- /dev/null
+++ b/src/utils/randomDayutils.ts
@@ -0,0 +1,22 @@
+/*---------------------------------------------------------
+ * Copyright 2022 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+// find a random day in the next 4 weeks, starting tomorrow
+// (If this code is changed, run the test in calendartest.ts
+// by hand.)
+export function promptNext4Weeks(now: Date): Date {
+	const day = 24 * 3600 * 1000; // milliseconds in a day
+	// choose a random day in the next 4 weeks, starting tomorrow
+	const delta = randomIntInRange(1, 28);
+	const x = now.getTime() + day * delta;
+	return new Date(x);
+}
+
+// randomIntInRange returns a random integer between min and max, inclusive.
+function randomIntInRange(min: number, max: number): number {
+	const low = Math.ceil(min);
+	const high = Math.floor(max);
+	return Math.floor(Math.random() * (high - low + 1)) + low;
+}
diff --git a/src/welcome.ts b/src/welcome.ts
index b40aa7e..4015f3a 100644
--- a/src/welcome.ts
+++ b/src/welcome.ts
@@ -186,11 +186,20 @@
 			<div class="Announcement">
 				<img src="${announceURI}" alt="announce" class="Announcement-image" />
 				<p>
-					New! <a href="https://github.com/golang/vscode-go/blob/master/docs/debugging.md#remote-debugging">Remote
-					attach debugging</a> is now available on demand via Delve's native DAP implementation with Delve v1.7.3 or newer.
-					We plan to enable this as the default in early 2022 to enhance remote debugging with the same
-					<a href="https://github.com/golang/vscode-go/blob/master/docs/debugging.md">debugging features</a>
-                    that are already in use for local debugging.
+					New!
+					We are excited to announce a new
+					<a href="https://github.com/golang/vscode-go/wiki/features#analyze-vulnerabilities-in-dependencies">code analysis feature</a>
+					that surfaces known vulnerabilities in your dependencies.
+					<br>
+					This vulncheck analyzer is backed by <a href="https://go.dev/security/vulndb">
+					Go's vulnerability database</a> and the Go language server's integration of
+					<a href="https://golang.org/x/vuln/cmd/govulncheck"><code>govulncheck</code></a>.
+					Read <a href="https://go.dev/blog/vuln">"Go's support for vulnerability management"</a>
+					to learn about the Go team's approach to helping Go developers secure their open-source dependencies.
+					Please share your feedback at
+					<a href="https://go.dev/s/vsc-vulncheck-feedback">go.dev/s/vsc-vulncheck-feedback</a>,
+					and report a bug in
+					<a href="https://github.com/golang/vscode-go/issues/new">our issue tracker</a>.
 				</p>
 			</div>
 
@@ -250,10 +259,10 @@
 function showGoWelcomePage() {
 	// Update this list of versions when there is a new version where we want to
 	// show the welcome page on update.
-	const showVersions: string[] = ['0.30.0'];
+	const showVersions: string[] = ['0.37.0'];
 	// TODO(hyangah): use the content hash instead of hard-coded string.
 	// https://github.com/golang/vscode-go/issue/1179
-	let goExtensionVersion = '0.30.0';
+	let goExtensionVersion = '0.37.0';
 	let goExtensionVersionKey = 'go.extensionVersion';
 	if (extensionInfo.isPreview) {
 		goExtensionVersion = '0.0.0';
diff --git a/syntaxes/govulncheck.tmGrammar.json b/syntaxes/govulncheck.tmGrammar.json
new file mode 100644
index 0000000..b557c59
--- /dev/null
+++ b/syntaxes/govulncheck.tmGrammar.json
@@ -0,0 +1,123 @@
+{
+	"scopeName": "govulncheck",
+	"patterns": [
+		{
+			"include": "#log"
+		},
+		{
+			"include": "#info"
+		},
+		{
+			"include": "#affecting"
+		},
+		{
+			"include": "#unaffecting"
+		},
+		{
+			"include": "#callstack"
+		},
+		{
+			"include": "#callstacklong"
+		},
+		{
+			"include": "#frame"
+		},
+		{
+			"include": "#framePosition"
+		}
+	],
+	"repository": {
+		"log": {
+			"comment": "log",
+			"match": "^\\d{2}:\\d{2}:\\d{2} \\S.*$",
+			"name": "comment.govulncheck"
+		},
+		"info": {
+			"comment": "info",
+			"match": "^# .*",
+			"name": "comment.govulncheck"
+		},
+		"affecting": {
+			"comment": "vulnerability heading",
+			"match": "^(⚠) (\\S+) \\((https://[^)]+)\\)",
+			"name": "markup.heading.1.govulncheck",
+			"captures": {
+				"1": {
+					"name": "token.error-token.severity.govulncheck"
+				},
+				"2": {
+					"name": "token.error-token.vulnid.govulncheck"
+				},
+				"3": {
+					"name": "entity.link.govulncheck"
+				}
+			}
+		},
+		"unaffecting": {
+			"comment": "vulnerability heading",
+			"match": "^(ⓘ) (\\S+) \\((https://[^)]+)\\)",
+			"captures": {
+				"1": {
+					"name": "token.info-token.severity.govulncheck"
+				},
+				"2": {
+					"name": "token.info-token.vulnid.govulncheck"
+				},
+				"3": {
+					"name": "entity.link.govulncheck"
+				}
+			}
+		},
+		"callstacklong": {
+			"comment": "callstack",
+			"match": "^\\- (\\S+) (\\S+) calls ([^,]+), which eventually calls (\\S+)",
+			"name": "markup.list.unnumbered.callstack.summary.govulncheck",
+			"captures": {
+				"1": {
+					"name": "markup.link.callstack.position.govulncheck"
+				},
+				"2": {
+					"name": "markup.italic.raw.callstack.symbol.govulncheck"
+				},
+				"3": {
+					"name": "markup.italic.callstack.symbol.govulncheck"
+				},
+				"4": {
+					"name": "markup.italic.callstack.symbol.govulncheck"
+				}
+			}
+		},
+		"callstack": {
+			"comment": "callstack",
+			"match": "^\\- (\\S+) (\\S+) calls ([^,]+)$",
+			"name": "markup.list.unnumbered.callstack.summary.govulncheck",
+			"captures": {
+				"1": {
+					"name": "markup.link.callstack.position.govulncheck"
+				},
+				"2": {
+					"name": "markup.italic.raw.callstack.symbol.govulncheck"
+				},
+				"3": {
+					"name": "markup.italic.callstack.symbol.govulncheck"
+				}
+			}
+		},
+		"frame": {
+			"comment": "frame",
+			"match": "^\\t(\\S+)",
+			"name": "markup.list.unnumbered.fram.govulncheck",
+			"captures": {
+				"1": { "name": "markup.italic.raw.callstack.symbol.govulncheck"}
+			}
+		},
+		"framePosition": {
+			"comment": "frame position info",
+			"match": "^\\t\\t(\\([^)]+\\))",
+			"name": "markup.list.unnumbered.frame.govulncheck",
+			"captures": {
+				"1": { "name": "comment.govulncheck"}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/test/gopls/extension.test.ts b/test/gopls/extension.test.ts
index 972e8ad..32245ce 100644
--- a/test/gopls/extension.test.ts
+++ b/test/gopls/extension.test.ts
@@ -92,18 +92,21 @@
 		});
 	}
 
-	public async setup(filePath: string) {
+	// Start the language server with the fakeOutputChannel.
+	public async startGopls(filePath: string, goConfig?: vscode.WorkspaceConfiguration) {
 		// file path to open.
 		this.fakeOutputChannel = new FakeOutputChannel();
 		const pkgLoadingDone = this.onMessageInTrace('Finished loading packages.', 60_000);
 
-		// Start the language server with the fakeOutputChannel.
-		const goConfig = Object.create(getGoConfig(), {
-			useLanguageServer: { value: true },
-			languageServerFlags: { value: ['-rpc.trace'] }, // enable rpc tracing to monitor progress reports
-			formatTool: { value: 'nonexistent' } // to test custom formatters
-		});
-		const cfg: BuildLanguageClientOption = buildLanguageServerConfig(goConfig);
+		if (!goConfig) {
+			goConfig = getGoConfig();
+		}
+		const cfg: BuildLanguageClientOption = buildLanguageServerConfig(
+			Object.create(goConfig, {
+				useLanguageServer: { value: true },
+				languageServerFlags: { value: ['-rpc.trace'] } // enable rpc tracing to monitor progress reports
+			})
+		);
 		cfg.outputChannel = this.fakeOutputChannel; // inject our fake output channel.
 		this.languageClient = await buildLanguageClient({}, cfg);
 		if (!this.languageClient) {
@@ -117,6 +120,7 @@
 
 	public async teardown() {
 		try {
+			await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
 			await this.languageClient?.stop(1_000); // 1s timeout
 		} catch (e) {
 			console.log(`failed to stop gopls within 1sec: ${e}`);
@@ -148,22 +152,24 @@
 	const projectDir = path.join(__dirname, '..', '..', '..');
 	const testdataDir = path.join(projectDir, 'test', 'testdata');
 	const env = new Env();
-
+	const sandbox = sinon.createSandbox();
 	let goVersion: GoVersion;
+
 	suiteSetup(async () => {
-		await env.setup(path.resolve(testdataDir, 'gogetdocTestData', 'test.go'));
 		goVersion = await getGoVersion();
 	});
-	suiteTeardown(() => env.teardown());
 
-	this.afterEach(function () {
+	this.afterEach(async function () {
+		await env.teardown();
 		// Note: this shouldn't use () => {...}. Arrow functions do not have 'this'.
 		// I don't know why but this.currentTest.state does not have the expected value when
 		// used with teardown.
 		env.flushTrace(this.currentTest?.state === 'failed');
+		sandbox.restore();
 	});
 
 	test('HoverProvider', async () => {
+		await env.startGopls(path.resolve(testdataDir, 'gogetdocTestData', 'test.go'));
 		const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'test.go');
 		const testCases: [string, vscode.Position, string | null, string | null][] = [
 			// [new vscode.Position(3,3), '/usr/local/go/src/fmt'],
@@ -213,6 +219,7 @@
 	});
 
 	test('Completion middleware', async () => {
+		await env.startGopls(path.resolve(testdataDir, 'gogetdocTestData', 'test.go'));
 		const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'test.go');
 		const testCases: [string, vscode.Position, string, vscode.CompletionItemKind][] = [
 			['fmt.P<>', new vscode.Position(19, 6), 'Print', vscode.CompletionItemKind.Function],
@@ -279,15 +286,45 @@
 		}
 	});
 
-	test('Nonexistent formatter', async () => {
-		const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'format.go');
-		const result = (await vscode.commands.executeCommand(
-			'vscode.executeFormatDocumentProvider',
-			uri,
-			{} // empty options
-		)) as vscode.TextEdit[];
-		if (result) {
-			assert.fail(`expected no result, got one: ${result}`);
+	async function testCustomFormatter(goConfig: vscode.WorkspaceConfiguration, customFormatter: string) {
+		const config = require('../../src/config');
+		sandbox.stub(config, 'getGoConfig').returns(goConfig);
+
+		await env.startGopls(path.resolve(testdataDir, 'gogetdocTestData', 'test.go'), goConfig);
+		const { doc } = await env.openDoc(testdataDir, 'gogetdocTestData', 'format.go');
+		await vscode.window.showTextDocument(doc);
+
+		const formatFeature = env.languageClient?.getFeature('textDocument/formatting');
+		const formatter = formatFeature?.getProvider(doc);
+		const tokensrc = new vscode.CancellationTokenSource();
+		try {
+			const result = await formatter?.provideDocumentFormattingEdits(
+				doc,
+				{} as vscode.FormattingOptions,
+				tokensrc.token
+			);
+			assert.fail(`formatter unexpectedly succeeded and returned a result: ${JSON.stringify(result)}`);
+		} catch (e) {
+			assert(`${e}`.includes(`errors when formatting with ${customFormatter}`), `${e}`);
 		}
+	}
+
+	test('Nonexistent formatter', async () => {
+		const customFormatter = 'nonexistent';
+		const goConfig = Object.create(getGoConfig(), {
+			formatTool: { value: customFormatter } // this should make the formatter fail.
+		}) as vscode.WorkspaceConfiguration;
+
+		await testCustomFormatter(goConfig, customFormatter);
+	});
+
+	test('Custom formatter', async () => {
+		const customFormatter = 'coolCustomFormatter';
+		const goConfig = Object.create(getGoConfig(), {
+			formatTool: { value: 'custom' }, // this should make the formatter fail.
+			alternateTools: { value: { customFormatter: customFormatter } } // this should make the formatter fail.
+		}) as vscode.WorkspaceConfiguration;
+
+		await testCustomFormatter(goConfig, customFormatter);
 	});
 });
diff --git a/test/gopls/vulncheck.test.ts b/test/gopls/vulncheck.test.ts
index 46699e5..b37711b 100644
--- a/test/gopls/vulncheck.test.ts
+++ b/test/gopls/vulncheck.test.ts
@@ -5,158 +5,145 @@
 import assert from 'assert';
 import path = require('path');
 import vscode = require('vscode');
-import { extensionId } from '../../src/const';
-import goVulncheck = require('../../src/goVulncheck');
+import { VulncheckOutputLinkProvider, VulncheckReport, writeVulns } from '../../src/goVulncheck';
+import fs = require('fs');
+import { CancellationTokenSource } from 'vscode-languageserver-protocol';
 
-suite('vulncheck result viewer tests', () => {
-	const webviewId = 'vulncheck';
-	const extensionUri = vscode.extensions.getExtension(extensionId)!.extensionUri;
+suite('writeVulns', () => {
 	const fixtureDir = path.join(__dirname, '..', '..', '..', 'test', 'testdata', 'vuln');
 
-	const disposables: vscode.Disposable[] = [];
-	function _register<T extends vscode.Disposable>(disposable: T) {
-		disposables.push(disposable);
-		return disposable;
-	}
-	let provider: goVulncheck.VulncheckResultViewProvider;
-
-	setup(() => {
-		provider = new goVulncheck.VulncheckResultViewProvider(extensionUri, {});
-	});
-
-	teardown(async () => {
-		await vscode.commands.executeCommand('workbench.action.closeAllEditors');
-		vscode.Disposable.from(...disposables).dispose();
-	});
-
-	test('populates webview', async () => {
-		const doTest = async (tag: string) => {
-			const webviewPanel = _register(
-				vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, {})
-			);
-			const source = path.join(fixtureDir, 'test.vulncheck.json');
-			const doc = await vscode.workspace.openTextDocument(source);
-			console.timeLog(tag, 'opened document');
-			const canceller = new vscode.CancellationTokenSource();
-			_register(canceller);
-
-			const watcher = getMessage<{ type: string; target?: string }>(webviewPanel);
-
-			await provider.resolveCustomTextEditor(doc, webviewPanel, canceller.token);
-			console.timeLog(tag, 'resolved custom text editor');
-
-			webviewPanel.reveal();
-
-			// Trigger snapshotContent that sends `snapshot-result` as a result.
-			webviewPanel.webview.postMessage({ type: 'snapshot-request' });
-			console.timeLog(tag, 'posted snapshot-request');
-
-			const res = await watcher;
-			console.timeLog(tag, 'received message');
-
-			assert.deepStrictEqual(res.type, 'snapshot-result', `want snapshot-result, got ${JSON.stringify(res)}`);
-			// res.target type is defined in vulncheckView.js.
-			const { log = '', vulns = '', unaffecting = '' } = JSON.parse(res.target ?? '{}');
-
-			assert(
-				log.includes('1 known vulnerabilities'),
-				`expected "1 known vulnerabilities", got ${JSON.stringify(res.target)}`
-			);
-			assert(
-				vulns.includes('GO-2021-0113') &&
-					vulns.includes('<td>Affecting</td><td>github.com/golang/vscode-go/test/testdata/vuln</td>'),
-				`expected "Affecting" section, got ${JSON.stringify(res.target)}`
-			);
-			// Unaffecting vulnerability's ID is reported.
-			assert(
-				unaffecting.includes('GO-2021-0000') && unaffecting.includes('golang.org/x/text'),
-				`expected reports about unaffecting vulns, got ${JSON.stringify(res.target)}`
-			);
-		};
-		try {
-			console.time('populates-webview');
-			await doTest('populates-webview');
-		} catch (e) {
-			console.timeLog('populates-webview', `error thrown: ${e}`);
-			throw e;
-		} finally {
-			console.timeEnd('populates-webview');
+	function testWriteVulns(res: VulncheckReport | undefined | null, expected: string | RegExp[]) {
+		const b = [] as string[];
+		writeVulns(res, { appendLine: (str) => b.push(str + '\n') });
+		const actual = b.join();
+		if ('string' === typeof expected) {
+			assert(actual.search(expected), `actual:\n${actual}\nwant:\n${expected}`);
+		} else {
+			// RegExp[]
+			expected.forEach((want) => assert(actual.match(want), `actual:\n${actual}\nwanted:${want}`));
 		}
-	}).timeout(5_000);
+	}
 
-	test('handles empty input', async () => {
-		const webviewPanel = _register(
-			vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, {})
-		);
-		// Empty doc.
-		const doc = await vscode.workspace.openTextDocument(
-			vscode.Uri.file('bogus.vulncheck.json').with({ scheme: 'untitled' })
-		);
-		const canceller = new vscode.CancellationTokenSource();
-		_register(canceller);
+	function readData(fname: string) {
+		const data = fs.readFileSync(fname);
+		return JSON.parse(data.toString());
+	}
 
-		const watcher = getMessage<{ type: string; target?: string }>(webviewPanel);
-
-		await provider.resolveCustomTextEditor(doc, webviewPanel, canceller.token);
-		webviewPanel.reveal();
-
-		// Trigger snapshotContent that sends `snapshot-result` as a result.
-		webviewPanel.webview.postMessage({ type: 'snapshot-request' });
-		const res = await watcher;
-		assert.deepStrictEqual(res.type, 'snapshot-result', `want snapshot-result, got ${JSON.stringify(res)}`);
-		const { log = '', vulns = '', unaffecting = '' } = JSON.parse(res.target ?? '{}');
-		assert(!log && !vulns && !unaffecting, res.target);
+	test('No vulnerability', () => testWriteVulns({}, 'No vulnerability found.\n'));
+	test('Undefined result', () => testWriteVulns(undefined, 'Error - invalid vulncheck result.\n'));
+	test('Nil result', () => testWriteVulns(null, 'Error - invalid vulncheck result.\n'));
+	test('Vulns is undefined', () => testWriteVulns({ Vulns: undefined }, 'No vulnerability found.\n'));
+	test('Vulns is empty', () => testWriteVulns({ Vulns: [] }, 'No vulnerability found.\n'));
+	test('Modules is empty', () =>
+		testWriteVulns({ Vulns: [{ OSV: { id: 'foo' }, Modules: [] }] }, 'No vulnerability found.\n'));
+	test('Nonaffecting', () => {
+		const vulns = readData(path.join(fixtureDir, 'vulncheck-result-unaffecting.json'));
+		testWriteVulns(vulns, [
+			/No vulnerability found\./s,
+			/# The vulnerabilities below are in packages that you import,/s,
+			/Found 1 unused vulnerability\./s,
+			/GO-2022-1059 \(https:\/\/[^)]+\)/s,
+			/Found Version: golang\.org\/x\/text@v0\.3\.7/s,
+			/Fixed Version: golang\.org\/x\/text@v0\.3\.8/s,
+			/Package: golang\.org\/x\/text\/language/s
+		]);
 	});
-
-	// TODO: test corrupted/incomplete json file handling.
+	test('Afffecting&Nonaffecting', () => {
+		const vulns = readData(path.join(fixtureDir, 'vulncheck-result-affecting.json'));
+		testWriteVulns(vulns, [
+			/Found 1 affecting vulnerability\./s,
+			/This is used/s, // details
+			/Found Version: golang\.org\/x\/text@v0\.3\.5/,
+			/Fixed Version: golang\.org\/x\/text@v0\.3\.7/,
+			/- main\.go:15:29: module2\.main calls/,
+			/\tmodule2.main\n/,
+			/\t\t\(.*\/main.go:15\)\n/,
+			/# The vulnerabilities below are in packages that you import,/s,
+			/Found 1 unused vulnerability\./s,
+			/GO-2022-1059 \(https:\/\/[^)]+\)/s,
+			/Found Version: golang\.org\/x\/text@v0\.3\.5/s,
+			/Fixed Version: golang\.org\/x\/text@v0\.3\.8/s,
+			/Package: golang\.org\/x\/text\/language/s
+		]);
+	});
 });
 
-function getMessage<R = { type: string; target?: string }>(webview: vscode.WebviewPanel): Promise<R> {
-	return new Promise<R>((resolve) => {
-		const sub = webview.webview.onDidReceiveMessage((message) => {
-			sub.dispose();
-			resolve(message);
-		});
-	});
-}
-suite('fillAffectedPkgs', () => {
-	test('compute from the first call stack entry', async () => {
-		const data = JSON.parse(`{
-		"Vuln": [{
-			"CallStacks": [
-				[
-				  {
-					"Name": "github.com/golang/vscode-go/test/testdata/vuln.main",
-					"URI": "file:///vuln/test.go",
-					"Pos": { "line": 9, "character": 0 }
-				  },
-				  {
-					"Name": "golang.org/x/text/language.Parse",
-					"URI": "file:///foo/bar.go",
-					"Pos": { "line": 227, "character": 0 }
-				  }
-				]
-			]}]}`);
-		goVulncheck.fillAffectedPkgs(data.Vuln);
-		assert.deepStrictEqual(data.Vuln[0].AffectedPkgs, ['github.com/golang/vscode-go/test/testdata/vuln']);
+const vulncheckOutput = `
+govulncheck ./... for file:///Users/foo/module3/go.mod
+08:10:27 Loading packages...
+08:10:28 Loaded 2 packages and their dependencies
+08:10:30 Found 1 affecting vulns and 0 unaffecting vulns in imported packages
+
+Found 1 affecting vulnerability.
+--------------------------------------------------------------------------------
+⚠ GO-2020-0040 (https://pkg.go.dev/vuln/GO-2020-0040)
+
+Due to unchecked type assertions, maliciously crafted messages can cause panics, which may be used as a denial of service vector. (CVE-2020-36562)
+
+Found Version: github.com/shiyanhui/dht@v0.0.0-20201219151056-5a20f3199263
+Fixed Version: N/A
+
+- lib/lib.go:28:21: module3/lib.Run$2 calls github.com/shiyanhui/dht.DHT.GetPeers
+	module3/lib.Run
+		(/home/user/module3/lib/lib.go:25)
+	module3/lib.Run$2
+		(/home/user/module3/lib/lib.go:28)`;
+
+suite.only('VulncheckOutputLinkProvider', () => {
+	let doc: vscode.TextDocument;
+	let links: vscode.DocumentLink[] | undefined | null;
+
+	suiteSetup(async () => {
+		doc = await vscode.workspace.openTextDocument({ content: vulncheckOutput, language: 'govulncheck' });
+
+		const p = new VulncheckOutputLinkProvider();
+		const tokenSrc = new CancellationTokenSource();
+		links = await p.provideDocumentLinks(doc, tokenSrc.token);
+		tokenSrc.dispose();
 	});
 
-	test('callstacks missing', async () => {
-		const data = JSON.parse('{ "Vuln": [{}] }');
-		goVulncheck.fillAffectedPkgs(data.Vuln);
-		assert.deepStrictEqual(data.Vuln[0].AffectedPkgs, []);
-	});
+	function checkExist(word: string, targetPattern: RegExp, tooltip?: string) {
+		assert(
+			links?.some((link) => {
+				const words = doc.getText(link.range);
 
-	test('callstacks empty', async () => {
-		const data = JSON.parse('{ "Vuln": [{"CallStacks": []}] }');
-		goVulncheck.fillAffectedPkgs(data.Vuln);
-		assert.deepStrictEqual(data.Vuln[0].AffectedPkgs, []);
+				return (
+					words.includes(word) &&
+					// TODO(hyangah): test the full URI matching. Currently, we do partial checking
+					// since behavior on windows needs more inspection.
+					link.target?.toString().search(targetPattern) &&
+					(!tooltip || link.tooltip === tooltip)
+				);
+			}),
+			`failed to find ${word} ${targetPattern} ${tooltip || ''}\n${JSON.stringify(links)}`
+		);
+	}
+	function checkNotExist(word: string) {
+		assert(
+			!links?.some((link) => {
+				const words = doc.getText(link.range);
+				return words === word;
+			}),
+			`got ${word} that shouldn't exist.\n${JSON.stringify(links)}`
+		);
+	}
+	test('provides links', () => {
+		assert(links, 'links are empty');
 	});
-
-	test('first call stack entry is missing Name', async () => {
-		const data = JSON.parse(`{
-		"Vuln": [{ "CallStacks": [ [ { "URI": "file:///vuln/test.go" } ] ]}]}`);
-		goVulncheck.fillAffectedPkgs(data.Vuln);
-		assert.deepStrictEqual(data.Vuln[0].AffectedPkgs, []);
+	test('linkify relative file link', () => {
+		checkExist('lib/lib.go:28:21', /module3.*lib.go#L28,21/);
+	});
+	test('linkify Found Version', () => {
+		checkExist(
+			'github.com/shiyanhui/dht@v0.0.0-20201219151056-5a20f3199263',
+			/pkg\.go\.dev\/github\.com\//,
+			'https://pkg.go.dev/github.com/shiyanhui/dht@v0.0.0-20201219151056-5a20f3199263'
+		);
+	});
+	test('linkify Fixed Version', () => {
+		checkNotExist('N/A');
+	});
+	test('linkify absolute paths', () => {
+		checkExist('/home/user/module3/lib/lib.go', /lib.go#L28/);
 	});
 });
diff --git a/test/integration/codelens.test.ts b/test/integration/codelens.test.ts
index e3e1b8f..7cf1398 100644
--- a/test/integration/codelens.test.ts
+++ b/test/integration/codelens.test.ts
@@ -64,21 +64,21 @@
 	test('Subtests - runs a test with cursor on t.Run line', async () => {
 		const editor = await vscode.window.showTextDocument(document);
 		editor.selection = new vscode.Selection(7, 4, 7, 4);
-		const result = await subTestAtCursor(ctx, {})([]);
+		const result = await subTestAtCursor('test')(ctx, {})([]);
 		assert.equal(result, true);
 	});
 
 	test('Subtests - runs a test with cursor within t.Run function', async () => {
 		const editor = await vscode.window.showTextDocument(document);
 		editor.selection = new vscode.Selection(8, 4, 8, 4);
-		const result = await subTestAtCursor(ctx, {})([]);
+		const result = await subTestAtCursor('test')(ctx, {})([]);
 		assert.equal(result, true);
 	});
 
 	test('Subtests - returns false for a failing test', async () => {
 		const editor = await vscode.window.showTextDocument(document);
 		editor.selection = new vscode.Selection(11, 4, 11, 4);
-		const result = await subTestAtCursor(ctx, {})([]);
+		const result = await subTestAtCursor('test')(ctx, {})([]);
 		assert.equal(result, false);
 	});
 
@@ -86,7 +86,7 @@
 		const editor = await vscode.window.showTextDocument(document);
 		editor.selection = new vscode.Selection(17, 4, 17, 4);
 		sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves(undefined);
-		const result = await subTestAtCursor(ctx, {})([]);
+		const result = await subTestAtCursor('test')(ctx, {})([]);
 		assert.equal(result, undefined);
 	});
 
@@ -94,28 +94,37 @@
 		const editor = await vscode.window.showTextDocument(document);
 		editor.selection = new vscode.Selection(17, 4, 17, 4);
 		sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('dynamic test name');
-		const result = await subTestAtCursor(ctx, {})([]);
+		const result = await subTestAtCursor('test')(ctx, {})([]);
 		assert.equal(result, false);
 	});
 
 	test('Subtests - does nothing when cursor outside of a test function', async () => {
 		const editor = await vscode.window.showTextDocument(document);
 		editor.selection = new vscode.Selection(5, 0, 5, 0);
-		const result = await subTestAtCursor(ctx, {})([]);
+		const result = await subTestAtCursor('test')(ctx, {})([]);
 		assert.equal(result, undefined);
 	});
 
 	test('Subtests - does nothing when no test function covers the cursor and a function name is passed in', async () => {
 		const editor = await vscode.window.showTextDocument(document);
 		editor.selection = new vscode.Selection(5, 0, 5, 0);
-		const result = await subTestAtCursor(ctx, {})({ functionName: 'TestMyFunction' });
+		const result = await subTestAtCursor('test')(ctx, {})({ functionName: 'TestMyFunction' });
 		assert.equal(result, undefined);
 	});
 
 	test('Test codelenses', async () => {
 		const codeLenses = await codeLensProvider.provideCodeLenses(document, cancellationTokenSource.token);
-		assert.equal(codeLenses.length, 4);
-		const wantCommands = ['go.test.package', 'go.test.file', 'go.test.cursor', 'go.debug.cursor'];
+		assert.equal(codeLenses.length, 8);
+		const wantCommands = [
+			'go.test.package',
+			'go.test.file',
+			'go.test.cursor',
+			'go.debug.cursor',
+			'go.subtest.cursor',
+			'go.debug.subtest.cursor',
+			'go.subtest.cursor',
+			'go.debug.subtest.cursor'
+		];
 		for (let i = 0; i < codeLenses.length; i++) {
 			assert.equal(codeLenses[i].command?.command, wantCommands[i]);
 		}
diff --git a/test/integration/coverage.test.ts b/test/integration/coverage.test.ts
index ef03be3..05970ac 100644
--- a/test/integration/coverage.test.ts
+++ b/test/integration/coverage.test.ts
@@ -9,9 +9,7 @@
 import assert from 'assert';
 import { applyCodeCoverageToAllEditors, coverageFilesForTest, initForTest } from '../../src/goCover';
 import { updateGoVarsFromConfig } from '../../src/goInstallTools';
-import fs = require('fs-extra');
 import path = require('path');
-import sinon = require('sinon');
 import vscode = require('vscode');
 
 // The ideal test would check that each open editor containing a file with coverage
@@ -39,6 +37,11 @@
 		const files = Object.keys(coverageFilesForTest());
 		const aDotGo = files.includes(path.join(fixtureSourcePath, 'a', 'a.go'));
 		const bDotGo = files.includes(path.join(fixtureSourcePath, 'b', 'b.go'));
-		assert.equal(aDotGo && bDotGo, true, `seen a.go:${aDotGo}, seen b.go:${bDotGo}\n${files}\n`);
+		// Coverage data (cover.out) contains a couple of bogus data with file name blah.go. They shouldn't appear.
+		const blahDotGo = files.includes(path.join(fixtureSourcePath, 'b', 'blah.go'));
+		assert(
+			aDotGo && bDotGo && !blahDotGo,
+			`!seen a.go:${aDotGo} or !seen b.go:${bDotGo} or seen blah.go:${blahDotGo}: ${files}\n`
+		);
 	});
 });
diff --git a/test/testdata/coverage/cover.out b/test/testdata/coverage/cover.out
index 3157dc6..0979574 100644
--- a/test/testdata/coverage/cover.out
+++ b/test/testdata/coverage/cover.out
@@ -1,3 +1,5 @@
 mode: set
 github.com/microsoft/vscode-go/gofixtures/coveragetest/a/a.go:19.71,22.25 3 1
 github.com/microsoft/vscode-go/gofixtures/coveragetest/b/b.go:35.2,35.14 1 1
+github.com/microsoft/vscode-go/gofixtures/coveragetest/b/blah.go:13.16,98989.0 2 1
+github.com/microsoft/vscode-go/gofixtures/coveragetest/b/blah.go:98992.0,98993.0 -2 1
\ No newline at end of file
diff --git a/test/testdata/vuln/test.vulncheck.json b/test/testdata/vuln/test.vulncheck.json
deleted file mode 100644
index 653c554..0000000
--- a/test/testdata/vuln/test.vulncheck.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
-  "Vuln": [
-    {
-      "ID": "GO-2021-0113",
-      "Details": "Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n",
-      "Aliases": [
-        "CVE-2021-38561"
-      ],
-      "Symbol": "Parse",
-      "PkgPath": "golang.org/x/text/language",
-      "ModPath": "golang.org/x/text",
-      "URL": "https://pkg.go.dev/vuln/GO-2021-0113",
-      "CurrentVersion": "v0.0.0-20170915032832-14c0d48ead0c",
-      "FixedVersion": "v0.3.7",
-      "AffectedPkgs": ["github.com/golang/vscode-go/test/testdata/vuln"],
-      "CallStacks": [
-        [
-          {
-            "Name": "github.com/golang/vscode-go/test/testdata/vuln.main",
-            "URI": "file:///Users/hakim/projects/vscode-go/test/testdata/vuln/test.go",
-            "Pos": {
-              "line": 9,
-              "character": 0
-            }
-          },
-          {
-            "Name": "golang.org/x/text/language.Parse",
-            "URI": "file:///Users/hakim/go/pkg/mod/golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c/language/parse.go",
-            "Pos": {
-              "line": 227,
-              "character": 0
-            }
-          }
-        ]
-      ],
-      "CallStackSummaries": [
-        "github.com/golang/vscode-go/test/testdata/vuln.main calls golang.org/x/text/language.Parse"
-      ]
-    },
-    {
-      "ID": "GO-2021-0000",
-      "Details": "Bogus Report",
-      "Symbol": "Parse",
-      "ModPath": "golang.org/x/text",
-      "URL": "https://pkg.go.dev/vuln/GO-2021-0000"
-    }
-  ],
-  "Start": "2022-05-16T13:43:54.437Z",
-  "Duration": 1407,
-  "Dir": "/Users/hakim/projects/vscode-go/test/testdata/vuln",
-  "Pattern": "./..."
-}
diff --git a/test/testdata/vuln/vulncheck-result-affecting.json b/test/testdata/vuln/vulncheck-result-affecting.json
new file mode 100644
index 0000000..04db9d5
--- /dev/null
+++ b/test/testdata/vuln/vulncheck-result-affecting.json
@@ -0,0 +1,94 @@
+{
+	"Vulns": [
+		{
+			"OSV": {
+				"id": "GO-2022-1059",
+				"details": "This is unaffecting",
+				"affected": [
+					{
+						"package": {
+							"name": "golang.org/x/text",
+							"ecosystem": "Go"
+						},
+						"database_specific": {
+							"url": "https://pkg.go.dev/vuln/GO-2022-1059"
+						}
+					}
+				]
+			},
+			"Modules": [
+				{
+					"Path": "golang.org/x/text",
+					"FoundVersion": "v0.3.5",
+					"FixedVersion": "v0.3.8",
+					"Packages": [
+						{
+							"Path": "golang.org/x/text/language",
+							"CallStacks": null
+						}
+					]
+				}
+			]
+		},
+		{
+			"OSV": {
+				"id": "GO-2021-0113",
+				"details": "This is used",
+				"affected": [
+					{
+						"package": {
+							"name": "golang.org/x/text",
+							"ecosystem": "Go"
+						},					
+						"database_specific": {
+							"url": "https://pkg.go.dev/vuln/GO-2021-0113"
+						}
+					}
+				]
+			},
+			"Modules": [
+				{
+					"Path": "golang.org/x/text",
+					"FoundVersion": "v0.3.5",
+					"FixedVersion": "v0.3.7",
+					"Packages": [
+						{
+							"Path": "golang.org/x/text/language",
+							"CallStacks": [
+								{
+									"Symbol": "Parse",
+									"Summary": "main.go:15:29: module2.main calls golang.org/x/text/language.Parse",
+									"Frames": [
+										{
+											"PkgPath": "module2",
+											"FuncName": "main",
+											"RecvType": "",
+											"Position": {
+												"Filename": "/Users/hakim/bugbash/govulncheck-study/module2/main.go",
+												"Offset": 230,
+												"Line": 15,
+												"Column": 29
+											}
+										},
+										{
+											"PkgPath": "golang.org/x/text/language",
+											"FuncName": "Parse",
+											"RecvType": "",
+											"Position": {
+												"Filename": "",
+												"Offset": 0,
+												"Line": 0,
+												"Column": 0
+											}
+										}
+									]
+								}
+							]
+						}
+					]
+				}
+			]
+		}
+	],
+	"Mode": "govulncheck"
+}
\ No newline at end of file
diff --git a/test/testdata/vuln/vulncheck-result-unaffecting.json b/test/testdata/vuln/vulncheck-result-unaffecting.json
new file mode 100644
index 0000000..72595ea
--- /dev/null
+++ b/test/testdata/vuln/vulncheck-result-unaffecting.json
@@ -0,0 +1,27 @@
+{
+	"Vulns": [
+		{
+			"OSV": {
+				"id": "GO-2022-1059",
+				"details": "Vulnerable!",
+				"database_specific": {
+					"url": "https://pkg.go.dev/vuln/GO-2022-1059"
+				}
+			},
+			"Modules": [
+				{
+					"Path": "golang.org/x/text",
+					"FoundVersion": "v0.3.7",
+					"FixedVersion": "v0.3.8",
+					"Packages": [
+						{
+							"Path": "golang.org/x/text/language",
+							"CallStacks": null
+						}
+					]
+				}
+			]
+		}
+	],
+	"Mode": "govulncheck"
+}
\ No newline at end of file
diff --git a/test/unit/calendartest.ts b/test/unit/calendartest.ts
new file mode 100644
index 0000000..abaea69
--- /dev/null
+++ b/test/unit/calendartest.ts
@@ -0,0 +1,66 @@
+/*---------------------------------------------------------
+ * Copyright 2022 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import assert from 'assert';
+import { promptNext4Weeks } from '../../src/utils/randomDayutils';
+
+// Set this to run the test. There's no point in running it over
+// and over, but it should be run if promptNext4Weeks is changed.
+const runNextDaysTest = false;
+
+// Test that a year of generated dates looks uniform. This test relies on
+// statistical tests, so in principle, it could fail. The parameters for the
+// statistics are chosen so there should be no more than a 1 in 1,000,000 chance.
+// (Further, if it passes once and the code it correct,
+// it then becomes a test of the random number generator, which is pointless.)
+// (this test takes about 40 msec on a 2018 Macbook Pro)
+suite('random day tests', () => {
+	test('next days', () => {
+		if (!runNextDaysTest) {
+			return;
+		}
+		const newYear = new Date('2024-01-01');
+		const day = 24 * 3600 * 1000;
+		const seen4 = new Array<number>(366);
+		for (let i = 0; i < 366; i++) {
+			seen4[i] = 0;
+		}
+		for (let i = 0; i < 366; i++) {
+			for (let j = 0; j < 100; j++) {
+				const today = new Date(newYear.getTime() + day * i);
+				const next = promptNext4Weeks(today);
+				assert((next.getTime() - today.getTime()) % day === 0);
+				const days = (next.getTime() - today.getTime()) / day;
+				assert(days >= 1 && days <= 28, 'days: ' + days);
+				const doy = Math.floor((next.getTime() - new Date(next.getFullYear(), 0, 0).getTime()) / day);
+				seen4[doy - 1]++;
+			}
+		}
+		assert.ok(isUniform(seen4));
+		// console.log(`elapsed ${new Date().getTime() - now.getTime()} ms}`);
+	});
+});
+
+// decide if the contnts of x look like a uniform distribution, This assumes x.length > 50 or so,
+// and approximates the chi-squared distribution with a normal distribution. (see the Wikipedia article)
+// The approximation is that sqrt(2*chi2) ~ N(sqrt(2*df-1) is good enough for our purposes.
+// The change of getting a 4.8 sigma deviation is about 1 in 1,000,000.
+function isUniform(x: number[], bound = 4.8): boolean {
+	const k = x.length;
+	const df = k - 1;
+	const sum = x.reduce((sum, current) => sum + current, 0);
+	const exp = sum / k;
+	const chi2 = x.reduce((sum, current) => sum + ((current - exp) * (current - exp)) / exp, 0);
+	const sd = Math.sqrt(2 * chi2) - Math.sqrt(2 * df - 1);
+	// sd would be the standard deviation in units of 1. 1
+	let ret = sd < bound && sd > -bound;
+	// and make sure the individual values aren't crazy (5 std devs of normal has prob 3e-7)
+	const expsd = 5 * Math.sqrt(exp);
+	x.map((v) => {
+		if (v < exp - expsd || v > exp + expsd) ret = false;
+	});
+	// console.log(`sd: ${sd} bound: ${bound} expsd: ${expsd} exp: ${exp} chi2: ${chi2} df: ${df}`);
+	return ret;
+}
diff --git a/test/unit/goDebug.test.ts b/test/unit/goDebug.test.ts
deleted file mode 100644
index 1ce6737..0000000
--- a/test/unit/goDebug.test.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-/*---------------------------------------------------------
- * Copyright (C) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See LICENSE in the project root for license information.
- *--------------------------------------------------------*/
-
-import assert from 'assert';
-import { findPathSeparator, normalizeSeparators } from '../../src/debugAdapter/goDebug';
-
-suite('NormalizeSeparators Tests', () => {
-	test('fix separator', () => {
-		const tt = [
-			{
-				input: 'path/to/file',
-				want: 'path/to/file'
-			},
-			{
-				input: '\\path\\to\\file',
-				want: '/path/to/file'
-			},
-			{
-				input: '/path/to\\file',
-				want: '/path/to/file'
-			}
-		];
-
-		for (const tc of tt) {
-			const got = normalizeSeparators(tc.input);
-			assert.strictEqual(got, tc.want);
-		}
-	});
-	test('fix drive casing', () => {
-		const tt = [
-			{
-				input: 'C:/path/to/file',
-				want: 'C:/path/to/file'
-			},
-			{
-				input: 'c:/path/to/file',
-				want: 'C:/path/to/file'
-			},
-			{
-				input: 'C:/path/to/file',
-				want: 'C:/path/to/file'
-			},
-			{
-				input: 'C:\\path\\to\\file',
-				want: 'C:/path/to/file'
-			},
-			{
-				input: 'c:\\path\\to\\file',
-				want: 'C:/path/to/file'
-			},
-			{
-				input: 'c:\\path\\to\\file',
-				want: 'C:/path/to/file'
-			}
-		];
-
-		for (const tc of tt) {
-			const got = normalizeSeparators(tc.input);
-			assert.strictEqual(got, tc.want);
-		}
-	});
-	test('relative paths', () => {
-		const tt = [
-			{
-				input: '../path/to/file',
-				want: '../path/to/file'
-			},
-			{
-				input: './path/to/file',
-				want: './path/to/file'
-			},
-			{
-				input: '..\\path\\to\\file',
-				want: '../path/to/file'
-			},
-			{
-				input: '.\\path\\to\\file',
-				want: './path/to/file'
-			},
-			{
-				input: '/path/to/../file',
-				want: '/path/to/../file'
-			},
-			{
-				input: 'c:\\path\\to\\..\\file',
-				want: 'C:/path/to/../file'
-			}
-		];
-
-		for (const tc of tt) {
-			const got = normalizeSeparators(tc.input);
-			assert.strictEqual(got, tc.want);
-		}
-	});
-
-	test('find path separator', () => {
-		const tt = [
-			{
-				input: '../path/to/file',
-				want: '/'
-			},
-			{
-				input: './path/to/file',
-				want: '/'
-			},
-			{
-				input: '..\\path\\to\\file',
-				want: '\\'
-			},
-			{
-				input: '.\\path\\to\\file',
-				want: '\\'
-			},
-			{
-				input: '/path/to/../file',
-				want: '/'
-			},
-			{
-				input: 'c:\\path\\to\\..\\file',
-				want: '\\'
-			},
-			{
-				input: '',
-				want: '/'
-			}
-		];
-
-		for (const tc of tt) {
-			const got = findPathSeparator(tc.input);
-			assert.strictEqual(got, tc.want);
-		}
-	});
-});
diff --git a/test/unit/subTestUtils.test.ts b/test/unit/subTestUtils.test.ts
new file mode 100644
index 0000000..7043cb4
--- /dev/null
+++ b/test/unit/subTestUtils.test.ts
@@ -0,0 +1,29 @@
+/*---------------------------------------------------------
+ * Copyright 2022 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import assert from 'assert';
+import { escapeSubTestName } from '../../src/subTestUtils';
+
+suite('escapeSubTestName Tests', () => {
+	test('correctly escapes sub tests', () => {
+		const tt = [
+			{
+				test: 'TestFunction',
+				subtest: 'GET /path/with/slashes',
+				want: '\\QTestFunction\\E$/^\\QGET_\\E$/^\\Qpath\\E$/^\\Qwith\\E$/^\\Qslashes\\E'
+			},
+			{
+				test: 'TestMain',
+				subtest: 'All{0,1} tests [run]+ (fine)',
+				want: '\\QTestMain\\E$/^\\QAll{0,1}_tests_[run]+_(fine)\\E'
+			}
+		];
+
+		for (const tc of tt) {
+			const got = escapeSubTestName(tc.test, tc.subtest);
+			assert.strictEqual(got, tc.want);
+		}
+	});
+});
diff --git a/tools/goplssetting/goplssetting.go b/tools/goplssetting/goplssetting.go
index c9125ee..c8b75f8 100644
--- a/tools/goplssetting/goplssetting.go
+++ b/tools/goplssetting/goplssetting.go
@@ -229,49 +229,15 @@
 			continue
 		}
 		for _, opt := range m[hierarchy] {
-			doc := opt.Doc
-			if mappedTo, ok := associatedToExtensionProperties[opt.Name]; ok {
-				doc = fmt.Sprintf("%v\nIf unspecified, values of `%v` will be propagated.\n", doc, strings.Join(mappedTo, ", "))
+			obj, err := toObject(opt)
+			if err != nil {
+				return nil, nil, err
 			}
-			obj := &Object{
-				MarkdownDescription: doc,
-				// TODO: are all gopls settings in the resource scope?
-				Scope: "resource",
-				// TODO: consider 'additionalProperties' if gopls api-json
-				// outputs acceptable properties.
-				// TODO: deprecation attribute
+			// TODO(hyangah): move diagnostic to all go.diagnostic.
+			if hierarchy == "ui.diagnostic" && opt.Name == "vulncheck" {
+				goProperties["go.diagnostic.vulncheck"] = obj
+				continue
 			}
-			// Handle any enum types.
-			if opt.Type == "enum" {
-				for _, v := range opt.EnumValues {
-					unquotedName, err := strconv.Unquote(v.Value)
-					if err != nil {
-						return nil, nil, err
-					}
-					obj.Enum = append(obj.Enum, unquotedName)
-					obj.MarkdownEnumDescriptions = append(obj.MarkdownEnumDescriptions, v.Doc)
-				}
-			}
-			// Handle any objects whose keys are enums.
-			if len(opt.EnumKeys.Keys) > 0 {
-				if obj.Properties == nil {
-					obj.Properties = map[string]*Object{}
-				}
-				for _, k := range opt.EnumKeys.Keys {
-					unquotedName, err := strconv.Unquote(k.Name)
-					if err != nil {
-						return nil, nil, err
-					}
-					obj.Properties[unquotedName] = &Object{
-						Type:                propertyType(opt.EnumKeys.ValueType),
-						MarkdownDescription: k.Doc,
-						Default:             formatDefault(k.Default, opt.EnumKeys.ValueType),
-					}
-				}
-			}
-			obj.Type = propertyType(opt.Type)
-			obj.Default = formatOptionDefault(opt)
-
 			key := opt.Name
 			if hierarchy != "" {
 				key = hierarchy + "." + key
@@ -282,6 +248,53 @@
 	return goplsProperties, goProperties, nil
 }
 
+func toObject(opt *OptionJSON) (*Object, error) {
+	doc := opt.Doc
+	if mappedTo, ok := associatedToExtensionProperties[opt.Name]; ok {
+		doc = fmt.Sprintf("%v\nIf unspecified, values of `%v` will be propagated.\n", doc, strings.Join(mappedTo, ", "))
+	}
+	obj := &Object{
+		MarkdownDescription: doc,
+		// TODO: are all gopls settings in the resource scope?
+		Scope: "resource",
+		// TODO: consider 'additionalProperties' if gopls api-json
+		// outputs acceptable properties.
+		// TODO: deprecation attribute
+	}
+	// Handle any enum types.
+	if opt.Type == "enum" {
+		for _, v := range opt.EnumValues {
+			unquotedName, err := strconv.Unquote(v.Value)
+			if err != nil {
+				return nil, err
+			}
+			obj.Enum = append(obj.Enum, unquotedName)
+			obj.MarkdownEnumDescriptions = append(obj.MarkdownEnumDescriptions, v.Doc)
+		}
+	}
+	// Handle any objects whose keys are enums.
+	if len(opt.EnumKeys.Keys) > 0 {
+		if obj.Properties == nil {
+			obj.Properties = map[string]*Object{}
+		}
+		for _, k := range opt.EnumKeys.Keys {
+			unquotedName, err := strconv.Unquote(k.Name)
+			if err != nil {
+				return nil, err
+			}
+			obj.Properties[unquotedName] = &Object{
+				Type:                propertyType(opt.EnumKeys.ValueType),
+				MarkdownDescription: k.Doc,
+				Default:             formatDefault(k.Default, opt.EnumKeys.ValueType),
+			}
+		}
+	}
+	obj.Type = propertyType(opt.Type)
+	obj.Default = formatOptionDefault(opt)
+
+	return obj, nil
+}
+
 func formatOptionDefault(opt *OptionJSON) interface{} {
 	// Each key will have its own default value, instead of one large global
 	// one. (Alternatively, we can build the default from the keys.)
diff --git a/tools/license.sh b/tools/license.sh
index ec2c4ea..d388323 100755
--- a/tools/license.sh
+++ b/tools/license.sh
@@ -38,6 +38,7 @@
   "Apache-2.0": 1,
   "BSD-2-Clause": 1,
   "BSD-3-Clause": 1,
+  "CC-BY-4.0": 1,
   "ISC": 1,
   "MIT": 1,
   "Unlicense": 1,