[gopls-release-branch.0.3] all: merge master into gopls-release-branch.0.3
In preparation for gopls/v0.3.3.
Change-Id: Ie25b15b914c7f11c685b068ef1b0b8786e299c92
diff --git a/cmd/benchcmp/benchcmp.go b/cmd/benchcmp/benchcmp.go
index 32f3a1c..ed53d71 100644
--- a/cmd/benchcmp/benchcmp.go
+++ b/cmd/benchcmp/benchcmp.go
@@ -32,6 +32,7 @@
`
func main() {
+ fmt.Fprintf(os.Stderr, "benchcmp is deprecated in favor of benchstat: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat\n")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: %s old.txt new.txt\n\n", os.Args[0])
flag.PrintDefaults()
diff --git a/cmd/compilebench/main.go b/cmd/compilebench/main.go
index f66cf87..df74357 100644
--- a/cmd/compilebench/main.go
+++ b/cmd/compilebench/main.go
@@ -246,7 +246,7 @@
var pkg Pkg
out, err := exec.Command(*flagGoCmd, "list", "-json", dir).Output()
if err != nil {
- return nil, fmt.Errorf("go list -json %s: %v\n", dir, err)
+ return nil, fmt.Errorf("go list -json %s: %v", dir, err)
}
if err := json.Unmarshal(out, &pkg); err != nil {
return nil, fmt.Errorf("go list -json %s: unmarshal: %v", dir, err)
diff --git a/cmd/digraph/digraph.go b/cmd/digraph/digraph.go
index ff2399f..88eb05b 100644
--- a/cmd/digraph/digraph.go
+++ b/cmd/digraph/digraph.go
@@ -417,7 +417,7 @@
case "succs", "preds":
if len(args) == 0 {
- return fmt.Errorf("usage: digraph %s <node> ...", cmd)
+ return fmt.Errorf("usage: digraph %s <node> ... ", cmd)
}
g := g
if cmd == "preds" {
@@ -435,7 +435,7 @@
case "forward", "reverse":
if len(args) == 0 {
- return fmt.Errorf("usage: digraph %s <node> ...", cmd)
+ return fmt.Errorf("usage: digraph %s <node> ... ", cmd)
}
roots := make(nodeset)
for _, root := range args {
diff --git a/cmd/fiximports/main.go b/cmd/fiximports/main.go
index b87a7d5..ece4adc 100644
--- a/cmd/fiximports/main.go
+++ b/cmd/fiximports/main.go
@@ -126,7 +126,7 @@
flag.Parse()
if len(flag.Args()) == 0 {
- fmt.Fprintf(stderr, usage)
+ fmt.Fprint(stderr, usage)
os.Exit(1)
}
if !fiximports(flag.Args()...) {
diff --git a/cmd/getgo/main.go b/cmd/getgo/main.go
index a92ae48..792ea05 100644
--- a/cmd/getgo/main.go
+++ b/cmd/getgo/main.go
@@ -27,7 +27,7 @@
version = "devel"
)
-var exitCleanly error = errors.New("exit cleanly sentinel value")
+var errExitCleanly error = errors.New("exit cleanly sentinel value")
func main() {
flag.Parse()
@@ -41,7 +41,7 @@
runStep := func(s step) {
err := s(ctx)
- if err == exitCleanly {
+ if err == errExitCleanly {
os.Exit(0)
}
if err != nil {
diff --git a/cmd/getgo/steps.go b/cmd/getgo/steps.go
index 41c57d2..e505f5a 100644
--- a/cmd/getgo/steps.go
+++ b/cmd/getgo/steps.go
@@ -25,7 +25,7 @@
}
if strings.ToLower(answer) != "y" {
fmt.Println("Exiting install.")
- return exitCleanly
+ return errExitCleanly
}
return nil
@@ -65,7 +65,7 @@
if strings.ToLower(answer) != "y" {
// TODO: handle passing a version
fmt.Println("Aborting install.")
- return exitCleanly
+ return errExitCleanly
}
return nil
@@ -79,7 +79,7 @@
if strings.ToLower(answer) != "y" {
fmt.Println("Aborting install.")
- return exitCleanly
+ return errExitCleanly
}
fmt.Printf("Downloading Go version %s to %s\n", *goVersion, installPath)
@@ -105,7 +105,7 @@
if strings.ToLower(answer) != "y" {
fmt.Println("Exiting and not setting up GOPATH.")
- return exitCleanly
+ return errExitCleanly
}
fmt.Println("Setting up GOPATH")
diff --git a/cmd/godex/gc.go b/cmd/godex/gc.go
index 95eba65..eaf0230 100644
--- a/cmd/godex/gc.go
+++ b/cmd/godex/gc.go
@@ -6,8 +6,11 @@
package main
-import "go/importer"
+import (
+ "go/importer"
+ "go/token"
+)
func init() {
- register("gc", importer.For("gc", nil))
+ register("gc", importer.ForCompiler(token.NewFileSet(), "gc", nil))
}
diff --git a/cmd/godex/gccgo.go b/cmd/godex/gccgo.go
index 7644998..a539f98 100644
--- a/cmd/godex/gccgo.go
+++ b/cmd/godex/gccgo.go
@@ -8,11 +8,12 @@
import (
"go/importer"
+ "go/token"
"go/types"
)
func init() {
- register("gccgo", importer.For("gccgo", nil))
+ register("gccgo", importer.ForCompiler(token.NewFileSet(), "gccgo", nil))
}
// Print the extra gccgo compiler data for this package, if it exists.
diff --git a/cmd/godex/godex.go b/cmd/godex/godex.go
index a222ed63..e1d7e2f 100644
--- a/cmd/godex/godex.go
+++ b/cmd/godex/godex.go
@@ -23,9 +23,9 @@
// lists of registered sources and corresponding importers
var (
- sources []string
- importers []types.Importer
- importFailed = errors.New("import failed")
+ sources []string
+ importers []types.Importer
+ errImportFailed = errors.New("import failed")
)
func usage() {
@@ -154,7 +154,7 @@
defer func() {
if recover() != nil {
pkg = nil
- err = importFailed
+ err = errImportFailed
}
}()
return p.imp.Import(path)
diff --git a/cmd/godex/writetype.go b/cmd/godex/writetype.go
index dd17f90..5cbe1b1 100644
--- a/cmd/godex/writetype.go
+++ b/cmd/godex/writetype.go
@@ -133,7 +133,7 @@
p.print("\n")
}
for i, n := 0, t.NumEmbeddeds(); i < n; i++ {
- typ := t.Embedded(i)
+ typ := t.EmbeddedType(i)
p.writeTypeInternal(this, typ, visited)
p.print("\n")
}
diff --git a/cmd/gotype/gotype.go b/cmd/gotype/gotype.go
index ed88e63..dbb2626 100644
--- a/cmd/gotype/gotype.go
+++ b/cmd/gotype/gotype.go
@@ -284,7 +284,7 @@
}
report(err)
},
- Importer: importer.For(*compiler, nil),
+ Importer: importer.ForCompiler(fset, *compiler, nil),
Sizes: SizesFor(build.Default.Compiler, build.Default.GOARCH),
}
diff --git a/container/intsets/sparse_test.go b/container/intsets/sparse_test.go
index e3ef9d3..7481a06 100644
--- a/container/intsets/sparse_test.go
+++ b/container/intsets/sparse_test.go
@@ -587,7 +587,7 @@
t.Errorf("shallow copy: recover() = %q, want %q", got, want)
}
}()
- y.String() // panics
+ _ = y.String() // panics
t.Error("didn't panic as expected")
}
diff --git a/go/analysis/doc.go b/go/analysis/doc.go
index 8fa4a85..ea56b72 100644
--- a/go/analysis/doc.go
+++ b/go/analysis/doc.go
@@ -1,6 +1,6 @@
/*
-The analysis package defines the interface between a modular static
+Package analysis defines the interface between a modular static
analysis and an analysis driver program.
diff --git a/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go b/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go
new file mode 100644
index 0000000..42250f9
--- /dev/null
+++ b/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go
@@ -0,0 +1,13 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The ifaceassert command runs the ifaceassert analyzer.
+package main
+
+import (
+ "golang.org/x/tools/go/analysis/passes/ifaceassert"
+ "golang.org/x/tools/go/analysis/singlechecker"
+)
+
+func main() { singlechecker.Main(ifaceassert.Analyzer) }
diff --git a/go/analysis/passes/ifaceassert/ifaceassert.go b/go/analysis/passes/ifaceassert/ifaceassert.go
new file mode 100644
index 0000000..c5a71a7
--- /dev/null
+++ b/go/analysis/passes/ifaceassert/ifaceassert.go
@@ -0,0 +1,101 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package ifaceassert defines an Analyzer that flags
+// impossible interface-interface type assertions.
+package ifaceassert
+
+import (
+ "go/ast"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
+)
+
+const Doc = `detect impossible interface-to-interface type assertions
+
+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:
+
+ var v interface {
+ Read()
+ }
+ _ = v.(io.Reader)
+
+The Read method in v has a different signature than the Read method in
+io.Reader, so this assertion cannot succeed.
+`
+
+var Analyzer = &analysis.Analyzer{
+ Name: "ifaceassert",
+ Doc: Doc,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: run,
+}
+
+// assertableTo checks whether interface v can be asserted into t. It returns
+// nil on success, or the first conflicting method on failure.
+func assertableTo(v, t types.Type) *types.Func {
+ // ensure that v and t are interfaces
+ V, _ := v.Underlying().(*types.Interface)
+ T, _ := t.Underlying().(*types.Interface)
+ if V == nil || T == nil {
+ return nil
+ }
+ if f, wrongType := types.MissingMethod(V, T, false); wrongType {
+ return f
+ }
+ return nil
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ nodeFilter := []ast.Node{
+ (*ast.TypeAssertExpr)(nil),
+ (*ast.TypeSwitchStmt)(nil),
+ }
+ inspect.Preorder(nodeFilter, func(n ast.Node) {
+ var (
+ assert *ast.TypeAssertExpr // v.(T) expression
+ targets []ast.Expr // interfaces T in v.(T)
+ )
+ switch n := n.(type) {
+ case *ast.TypeAssertExpr:
+ // take care of v.(type) in *ast.TypeSwitchStmt
+ if n.Type == nil {
+ return
+ }
+ assert = n
+ targets = append(targets, n.Type)
+ case *ast.TypeSwitchStmt:
+ // retrieve type assertion from type switch's 'assign' field
+ switch t := n.Assign.(type) {
+ case *ast.ExprStmt:
+ assert = t.X.(*ast.TypeAssertExpr)
+ case *ast.AssignStmt:
+ assert = t.Rhs[0].(*ast.TypeAssertExpr)
+ }
+ // gather target types from case clauses
+ for _, c := range n.Body.List {
+ targets = append(targets, c.(*ast.CaseClause).List...)
+ }
+ }
+ V := pass.TypesInfo.TypeOf(assert.X)
+ for _, target := range targets {
+ T := pass.TypesInfo.TypeOf(target)
+ if f := assertableTo(V, T); f != nil {
+ pass.Reportf(
+ target.Pos(),
+ "impossible type assertion: no type can implement both %v and %v (conflicting types for %v method)",
+ V, T, f.Name(),
+ )
+ }
+ }
+ })
+ return nil, nil
+}
diff --git a/go/analysis/passes/ifaceassert/ifaceassert_test.go b/go/analysis/passes/ifaceassert/ifaceassert_test.go
new file mode 100644
index 0000000..4607338
--- /dev/null
+++ b/go/analysis/passes/ifaceassert/ifaceassert_test.go
@@ -0,0 +1,17 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ifaceassert_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+ "golang.org/x/tools/go/analysis/passes/ifaceassert"
+)
+
+func Test(t *testing.T) {
+ testdata := analysistest.TestData()
+ analysistest.Run(t, testdata, ifaceassert.Analyzer, "a")
+}
diff --git a/go/analysis/passes/ifaceassert/testdata/src/a/a.go b/go/analysis/passes/ifaceassert/testdata/src/a/a.go
new file mode 100644
index 0000000..ca9beb0
--- /dev/null
+++ b/go/analysis/passes/ifaceassert/testdata/src/a/a.go
@@ -0,0 +1,40 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains tests for the ifaceassert checker.
+
+package a
+
+import "io"
+
+func InterfaceAssertionTest() {
+ var (
+ a io.ReadWriteSeeker
+ b interface {
+ Read()
+ Write()
+ }
+ )
+ _ = a.(io.Reader)
+ _ = a.(io.ReadWriter)
+ _ = b.(io.Reader) // want `^impossible type assertion: no type can implement both interface{Read\(\); Write\(\)} and io.Reader \(conflicting types for Read method\)$`
+ _ = b.(interface { // want `^impossible type assertion: no type can implement both interface{Read\(\); Write\(\)} and interface{Read\(p \[\]byte\) \(n int, err error\)} \(conflicting types for Read method\)$`
+ Read(p []byte) (n int, err error)
+ })
+
+ switch a.(type) {
+ case io.ReadWriter:
+ case interface { // want `^impossible type assertion: no type can implement both io.ReadWriteSeeker and interface{Write\(\)} \(conflicting types for Write method\)$`
+ Write()
+ }:
+ default:
+ }
+
+ switch b := b.(type) {
+ case io.ReadWriter, interface{ Read() }: // want `^impossible type assertion: no type can implement both interface{Read\(\); Write\(\)} and io.ReadWriter \(conflicting types for Read method\)$`
+ case io.Writer: // want `^impossible type assertion: no type can implement both interface{Read\(\); Write\(\)} and io.Writer \(conflicting types for Write method\)$`
+ default:
+ _ = b
+ }
+}
diff --git a/go/analysis/passes/stringintconv/cmd/stringintconv/main.go b/go/analysis/passes/stringintconv/cmd/stringintconv/main.go
new file mode 100644
index 0000000..118b957
--- /dev/null
+++ b/go/analysis/passes/stringintconv/cmd/stringintconv/main.go
@@ -0,0 +1,13 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The stringintconv command runs the stringintconv analyzer.
+package main
+
+import (
+ "golang.org/x/tools/go/analysis/passes/stringintconv"
+ "golang.org/x/tools/go/analysis/singlechecker"
+)
+
+func main() { singlechecker.Main(stringintconv.Analyzer) }
diff --git a/go/analysis/passes/stringintconv/string.go b/go/analysis/passes/stringintconv/string.go
new file mode 100644
index 0000000..ac2cd84
--- /dev/null
+++ b/go/analysis/passes/stringintconv/string.go
@@ -0,0 +1,126 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package stringintconv defines an Analyzer that flags type conversions
+// from integers to strings.
+package stringintconv
+
+import (
+ "fmt"
+ "go/ast"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
+)
+
+const Doc = `check for string(int) conversions
+
+This checker flags conversions of the form string(x) where x is an integer
+(but not byte or rune) type. Such conversions are discouraged because they
+return the UTF-8 representation of the Unicode code point x, and not a decimal
+string representation of x as one might expect. Furthermore, if x denotes an
+invalid code point, the conversion cannot be statically rejected.
+
+For conversions that intend on using the code point, consider replacing them
+with string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the
+string representation of the value in the desired base.
+`
+
+var Analyzer = &analysis.Analyzer{
+ Name: "stringintconv",
+ Doc: Doc,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: run,
+}
+
+func typeName(typ types.Type) string {
+ if v, _ := typ.(interface{ Name() string }); v != nil {
+ return v.Name()
+ }
+ if v, _ := typ.(interface{ Obj() *types.TypeName }); v != nil {
+ return v.Obj().Name()
+ }
+ return ""
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ nodeFilter := []ast.Node{
+ (*ast.CallExpr)(nil),
+ }
+ inspect.Preorder(nodeFilter, func(n ast.Node) {
+ call := n.(*ast.CallExpr)
+
+ // Retrieve target type name.
+ var tname *types.TypeName
+ switch fun := call.Fun.(type) {
+ case *ast.Ident:
+ tname, _ = pass.TypesInfo.Uses[fun].(*types.TypeName)
+ case *ast.SelectorExpr:
+ tname, _ = pass.TypesInfo.Uses[fun.Sel].(*types.TypeName)
+ }
+ if tname == nil {
+ return
+ }
+ target := tname.Name()
+
+ // Check that target type T in T(v) has an underlying type of string.
+ T, _ := tname.Type().Underlying().(*types.Basic)
+ if T == nil || T.Kind() != types.String {
+ return
+ }
+ if s := T.Name(); target != s {
+ target += " (" + s + ")"
+ }
+
+ // Check that type V of v has an underlying integral type that is not byte or rune.
+ if len(call.Args) != 1 {
+ return
+ }
+ v := call.Args[0]
+ vtyp := pass.TypesInfo.TypeOf(v)
+ V, _ := vtyp.Underlying().(*types.Basic)
+ if V == nil || V.Info()&types.IsInteger == 0 {
+ return
+ }
+ switch V.Kind() {
+ case types.Byte, types.Rune, types.UntypedRune:
+ return
+ }
+
+ // Retrieve source type name.
+ source := typeName(vtyp)
+ if source == "" {
+ return
+ }
+ if s := V.Name(); source != s {
+ source += " (" + s + ")"
+ }
+ diag := analysis.Diagnostic{
+ Pos: n.Pos(),
+ Message: fmt.Sprintf("conversion from %s to %s yields a string of one rune", source, target),
+ SuggestedFixes: []analysis.SuggestedFix{
+ {
+ Message: "Did you mean to convert a rune to a string?",
+ TextEdits: []analysis.TextEdit{
+ {
+ Pos: v.Pos(),
+ End: v.Pos(),
+ NewText: []byte("rune("),
+ },
+ {
+ Pos: v.End(),
+ End: v.End(),
+ NewText: []byte(")"),
+ },
+ },
+ },
+ },
+ }
+ pass.Report(diag)
+ })
+ return nil, nil
+}
diff --git a/go/analysis/passes/stringintconv/string_test.go b/go/analysis/passes/stringintconv/string_test.go
new file mode 100644
index 0000000..ed06332
--- /dev/null
+++ b/go/analysis/passes/stringintconv/string_test.go
@@ -0,0 +1,17 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package stringintconv_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+ "golang.org/x/tools/go/analysis/passes/stringintconv"
+)
+
+func Test(t *testing.T) {
+ testdata := analysistest.TestData()
+ analysistest.Run(t, testdata, stringintconv.Analyzer, "a")
+}
diff --git a/go/analysis/passes/stringintconv/testdata/src/a/a.go b/go/analysis/passes/stringintconv/testdata/src/a/a.go
new file mode 100644
index 0000000..72ceb97
--- /dev/null
+++ b/go/analysis/passes/stringintconv/testdata/src/a/a.go
@@ -0,0 +1,36 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains tests for the stringintconv checker.
+
+package a
+
+type A string
+
+type B = string
+
+type C int
+
+type D = uintptr
+
+func StringTest() {
+ var (
+ i int
+ j rune
+ k byte
+ l C
+ m D
+ n = []int{0, 1, 2}
+ o struct{ x int }
+ )
+ const p = 0
+ _ = string(i) // want `^conversion from int to string yields a string of one rune$`
+ _ = string(j)
+ _ = string(k)
+ _ = string(p) // want `^conversion from untyped int to string yields a string of one rune$`
+ _ = A(l) // want `^conversion from C \(int\) to A \(string\) yields a string of one rune$`
+ _ = B(m) // want `^conversion from uintptr to B \(string\) yields a string of one rune$`
+ _ = string(n[1]) // want `^conversion from int to string yields a string of one rune$`
+ _ = string(o.x) // want `^conversion from int to string yields a string of one rune$`
+}
diff --git a/go/cfg/cfg.go b/go/cfg/cfg.go
index b075034..3ebc65f 100644
--- a/go/cfg/cfg.go
+++ b/go/cfg/cfg.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// This package constructs a simple control-flow graph (CFG) of the
+// Package cfg constructs a simple control-flow graph (CFG) of the
// statements and expressions within a single function.
//
// Use cfg.New to construct the CFG for a function body.
diff --git a/go/expect/expect.go b/go/expect/expect.go
index ac9975c..672c3a3 100644
--- a/go/expect/expect.go
+++ b/go/expect/expect.go
@@ -56,6 +56,7 @@
"bytes"
"fmt"
"go/token"
+ "path/filepath"
"regexp"
)
@@ -89,12 +90,20 @@
return token.NoPos, token.NoPos, fmt.Errorf("invalid file: %v", err)
}
position := f.Position(end)
- startOffset := f.Offset(lineStart(f, position.Line))
+ startOffset := f.Offset(f.LineStart(position.Line))
endOffset := f.Offset(end)
line := content[startOffset:endOffset]
matchStart, matchEnd := -1, -1
switch pattern := pattern.(type) {
case string:
+ // If the file is a go.mod and we are matching // indirect, then we
+ // need to look for it on the line after the current line.
+ // TODO(golang/go#36894): have a more intuitive approach for // indirect
+ if filepath.Ext(f.Name()) == ".mod" && pattern == "// indirect" {
+ startOffset = f.Offset(f.LineStart(position.Line + 1))
+ endOffset = f.Offset(lineEnd(f, position.Line+1))
+ line = content[startOffset:endOffset]
+ }
bytePattern := []byte(pattern)
matchStart = bytes.Index(line, bytePattern)
if matchStart >= 0 {
@@ -118,32 +127,9 @@
return f.Pos(startOffset + matchStart), f.Pos(startOffset + matchEnd), nil
}
-// this functionality was borrowed from the analysisutil package
-func lineStart(f *token.File, line int) token.Pos {
- // Use binary search to find the start offset of this line.
- //
- // TODO(adonovan): eventually replace this function with the
- // simpler and more efficient (*go/token.File).LineStart, added
- // in go1.12.
-
- min := 0 // inclusive
- max := f.Size() // exclusive
- for {
- offset := (min + max) / 2
- pos := f.Pos(offset)
- posn := f.Position(pos)
- if posn.Line == line {
- return pos - (token.Pos(posn.Column) - 1)
- }
-
- if min+1 >= max {
- return token.NoPos
- }
-
- if posn.Line < line {
- min = offset
- } else {
- max = offset
- }
+func lineEnd(f *token.File, line int) token.Pos {
+ if line >= f.LineCount() {
+ return token.Pos(f.Base() + f.Size())
}
+ return f.LineStart(line + 1)
}
diff --git a/go/expect/expect_test.go b/go/expect/expect_test.go
index c00a0d7..bd6e437 100644
--- a/go/expect/expect_test.go
+++ b/go/expect/expect_test.go
@@ -14,102 +14,127 @@
)
func TestMarker(t *testing.T) {
- const filename = "testdata/test.go"
- content, err := ioutil.ReadFile(filename)
- if err != nil {
- t.Fatal(err)
- }
+ for _, tt := range []struct {
+ filename string
+ expectNotes int
+ expectMarkers map[string]string
+ expectChecks map[string][]interface{}
+ }{
+ {
+ filename: "testdata/test.go",
+ expectNotes: 13,
+ expectMarkers: map[string]string{
+ "αSimpleMarker": "α",
+ "OffsetMarker": "β",
+ "RegexMarker": "γ",
+ "εMultiple": "ε",
+ "ζMarkers": "ζ",
+ "ηBlockMarker": "η",
+ "Declared": "η",
+ "Comment": "ι",
+ "LineComment": "someFunc",
+ "NonIdentifier": "+",
+ "StringMarker": "\"hello\"",
+ },
+ expectChecks: map[string][]interface{}{
+ "αSimpleMarker": nil,
+ "StringAndInt": []interface{}{"Number %d", int64(12)},
+ "Bool": []interface{}{true},
+ },
+ },
+ {
+ filename: "testdata/go.mod",
+ expectNotes: 3,
+ expectMarkers: map[string]string{
+ "αMarker": "αfake1α",
+ "IndirectMarker": "// indirect",
+ "βMarker": "require golang.org/modfile v0.0.0",
+ },
+ },
+ } {
+ t.Run(tt.filename, func(t *testing.T) {
+ content, err := ioutil.ReadFile(tt.filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ readFile := func(string) ([]byte, error) { return content, nil }
- const expectNotes = 13
- expectMarkers := map[string]string{
- "αSimpleMarker": "α",
- "OffsetMarker": "β",
- "RegexMarker": "γ",
- "εMultiple": "ε",
- "ζMarkers": "ζ",
- "ηBlockMarker": "η",
- "Declared": "η",
- "Comment": "ι",
- "LineComment": "someFunc",
- "NonIdentifier": "+",
- "StringMarker": "\"hello\"",
- }
- expectChecks := map[string][]interface{}{
- "αSimpleMarker": nil,
- "StringAndInt": []interface{}{"Number %d", int64(12)},
- "Bool": []interface{}{true},
- }
-
- readFile := func(string) ([]byte, error) { return content, nil }
- markers := make(map[string]token.Pos)
- for name, tok := range expectMarkers {
- offset := bytes.Index(content, []byte(tok))
- markers[name] = token.Pos(offset + 1)
- end := bytes.Index(content[offset:], []byte(tok))
- if end > 0 {
- markers[name+"@"] = token.Pos(offset + end + 2)
- }
- }
-
- fset := token.NewFileSet()
- notes, err := expect.Parse(fset, filename, nil)
- if err != nil {
- t.Fatalf("Failed to extract notes: %v", err)
- }
- if len(notes) != expectNotes {
- t.Errorf("Expected %v notes, got %v", expectNotes, len(notes))
- }
- for _, n := range notes {
- switch {
- case n.Args == nil:
- // A //@foo note associates the name foo with the position of the
- // first match of "foo" on the current line.
- checkMarker(t, fset, readFile, markers, n.Pos, n.Name, n.Name)
- case n.Name == "mark":
- // A //@mark(name, "pattern") note associates the specified name
- // with the position on the first match of pattern on the current line.
- if len(n.Args) != 2 {
- t.Errorf("%v: expected 2 args to mark, got %v", fset.Position(n.Pos), len(n.Args))
- continue
- }
- ident, ok := n.Args[0].(expect.Identifier)
- if !ok {
- t.Errorf("%v: identifier, got %T", fset.Position(n.Pos), n.Args[0])
- continue
- }
- checkMarker(t, fset, readFile, markers, n.Pos, string(ident), n.Args[1])
-
- case n.Name == "check":
- // A //@check(args, ...) note specifies some hypothetical action to
- // be taken by the test driver and its expected outcome.
- // In this test, the action is to compare the arguments
- // against expectChecks.
- if len(n.Args) < 1 {
- t.Errorf("%v: expected 1 args to check, got %v", fset.Position(n.Pos), len(n.Args))
- continue
- }
- ident, ok := n.Args[0].(expect.Identifier)
- if !ok {
- t.Errorf("%v: identifier, got %T", fset.Position(n.Pos), n.Args[0])
- continue
- }
- args, ok := expectChecks[string(ident)]
- if !ok {
- t.Errorf("%v: unexpected check %v", fset.Position(n.Pos), ident)
- continue
- }
- if len(n.Args) != len(args)+1 {
- t.Errorf("%v: expected %v args to check, got %v", fset.Position(n.Pos), len(args)+1, len(n.Args))
- continue
- }
- for i, got := range n.Args[1:] {
- if args[i] != got {
- t.Errorf("%v: arg %d expected %v, got %v", fset.Position(n.Pos), i, args[i], got)
+ markers := make(map[string]token.Pos)
+ for name, tok := range tt.expectMarkers {
+ offset := bytes.Index(content, []byte(tok))
+ // Handle special case where we look for // indirect and we
+ // need to search the next line.
+ if tok == "// indirect" {
+ offset = bytes.Index(content, []byte(" "+tok)) + 1
+ }
+ markers[name] = token.Pos(offset + 1)
+ end := bytes.Index(content[offset:], []byte(tok))
+ if end > 0 {
+ markers[name+"@"] = token.Pos(offset + end + 2)
}
}
- default:
- t.Errorf("Unexpected note %v at %v", n.Name, fset.Position(n.Pos))
- }
+
+ fset := token.NewFileSet()
+ notes, err := expect.Parse(fset, tt.filename, content)
+ if err != nil {
+ t.Fatalf("Failed to extract notes: %v", err)
+ }
+ if len(notes) != tt.expectNotes {
+ t.Errorf("Expected %v notes, got %v", tt.expectNotes, len(notes))
+ }
+ for _, n := range notes {
+ switch {
+ case n.Args == nil:
+ // A //@foo note associates the name foo with the position of the
+ // first match of "foo" on the current line.
+ checkMarker(t, fset, readFile, markers, n.Pos, n.Name, n.Name)
+ case n.Name == "mark":
+ // A //@mark(name, "pattern") note associates the specified name
+ // with the position on the first match of pattern on the current line.
+ if len(n.Args) != 2 {
+ t.Errorf("%v: expected 2 args to mark, got %v", fset.Position(n.Pos), len(n.Args))
+ continue
+ }
+ ident, ok := n.Args[0].(expect.Identifier)
+ if !ok {
+ t.Errorf("%v: identifier, got %T", fset.Position(n.Pos), n.Args[0])
+ continue
+ }
+ checkMarker(t, fset, readFile, markers, n.Pos, string(ident), n.Args[1])
+
+ case n.Name == "check":
+ // A //@check(args, ...) note specifies some hypothetical action to
+ // be taken by the test driver and its expected outcome.
+ // In this test, the action is to compare the arguments
+ // against expectChecks.
+ if len(n.Args) < 1 {
+ t.Errorf("%v: expected 1 args to check, got %v", fset.Position(n.Pos), len(n.Args))
+ continue
+ }
+ ident, ok := n.Args[0].(expect.Identifier)
+ if !ok {
+ t.Errorf("%v: identifier, got %T", fset.Position(n.Pos), n.Args[0])
+ continue
+ }
+ args, ok := tt.expectChecks[string(ident)]
+ if !ok {
+ t.Errorf("%v: unexpected check %v", fset.Position(n.Pos), ident)
+ continue
+ }
+ if len(n.Args) != len(args)+1 {
+ t.Errorf("%v: expected %v args to check, got %v", fset.Position(n.Pos), len(args)+1, len(n.Args))
+ continue
+ }
+ for i, got := range n.Args[1:] {
+ if args[i] != got {
+ t.Errorf("%v: arg %d expected %v, got %v", fset.Position(n.Pos), i, args[i], got)
+ }
+ }
+ default:
+ t.Errorf("Unexpected note %v at %v", n.Name, fset.Position(n.Pos))
+ }
+ }
+ })
}
}
diff --git a/go/expect/extract.go b/go/expect/extract.go
index 249369f..4862b76 100644
--- a/go/expect/extract.go
+++ b/go/expect/extract.go
@@ -9,15 +9,17 @@
"go/ast"
"go/parser"
"go/token"
+ "path/filepath"
"regexp"
"strconv"
"strings"
"text/scanner"
+
+ "golang.org/x/mod/modfile"
)
-const (
- commentStart = "@"
-)
+const commentStart = "@"
+const commentStartLen = len(commentStart)
// Identifier is the type for an identifier in an Note argument list.
type Identifier string
@@ -34,52 +36,72 @@
if content != nil {
src = content
}
- // TODO: We should write this in terms of the scanner.
- // there are ways you can break the parser such that it will not add all the
- // comments to the ast, which may result in files where the tests are silently
- // not run.
- file, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
- if file == nil {
- return nil, err
+ switch filepath.Ext(filename) {
+ case ".go":
+ // TODO: We should write this in terms of the scanner.
+ // there are ways you can break the parser such that it will not add all the
+ // comments to the ast, which may result in files where the tests are silently
+ // not run.
+ file, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
+ if file == nil {
+ return nil, err
+ }
+ return ExtractGo(fset, file)
+ case ".mod":
+ file, err := modfile.Parse(filename, content, nil)
+ if err != nil {
+ return nil, err
+ }
+ f := fset.AddFile(filename, -1, len(content))
+ f.SetLinesForContent(content)
+ notes, err := extractMod(fset, file)
+ if err != nil {
+ return nil, err
+ }
+ // Since modfile.Parse does not return an *ast, we need to add the offset
+ // within the file's contents to the file's base relative to the fileset.
+ for _, note := range notes {
+ note.Pos += token.Pos(f.Base())
+ }
+ return notes, nil
}
- return Extract(fset, file)
+ return nil, nil
}
-// Extract collects all the notes present in an AST.
+// extractMod collects all the notes present in a go.mod file.
// Each comment whose text starts with @ is parsed as a comma-separated
// sequence of notes.
// See the package documentation for details about the syntax of those
// notes.
-func Extract(fset *token.FileSet, file *ast.File) ([]*Note, error) {
+// Only allow notes to appear with the following format: "//@mark()" or // @mark()
+func extractMod(fset *token.FileSet, file *modfile.File) ([]*Note, error) {
var notes []*Note
- for _, g := range file.Comments {
- for _, c := range g.List {
- text := c.Text
- if strings.HasPrefix(text, "/*") {
- text = strings.TrimSuffix(text, "*/")
- }
- text = text[2:] // remove "//" or "/*" prefix
-
- // Allow notes to appear within comments.
- // For example:
- // "// //@mark()" is valid.
- // "// @mark()" is not valid.
- // "// /*@mark()*/" is not valid.
- var adjust int
- if i := strings.Index(text, commentStart); i > 2 {
- // Get the text before the commentStart.
- pre := text[i-2 : i]
- if pre != "//" {
- continue
- }
- text = text[i:]
- adjust = i
- }
- if !strings.HasPrefix(text, commentStart) {
+ for _, stmt := range file.Syntax.Stmt {
+ comment := stmt.Comment()
+ if comment == nil {
+ continue
+ }
+ // Handle the case for markers of `// indirect` to be on the line before
+ // the require statement.
+ // TODO(golang/go#36894): have a more intuitive approach for // indirect
+ for _, cmt := range comment.Before {
+ text, adjust := getAdjustedNote(cmt.Token)
+ if text == "" {
continue
}
- text = text[len(commentStart):]
- parsed, err := parse(fset, token.Pos(int(c.Pos())+4+adjust), text)
+ parsed, err := parse(fset, token.Pos(int(cmt.Start.Byte)+adjust), text)
+ if err != nil {
+ return nil, err
+ }
+ notes = append(notes, parsed...)
+ }
+ // Handle the normal case for markers on the same line.
+ for _, cmt := range comment.Suffix {
+ text, adjust := getAdjustedNote(cmt.Token)
+ if text == "" {
+ continue
+ }
+ parsed, err := parse(fset, token.Pos(int(cmt.Start.Byte)+adjust), text)
if err != nil {
return nil, err
}
@@ -89,6 +111,57 @@
return notes, nil
}
+// ExtractGo collects all the notes present in an AST.
+// Each comment whose text starts with @ is parsed as a comma-separated
+// sequence of notes.
+// See the package documentation for details about the syntax of those
+// notes.
+func ExtractGo(fset *token.FileSet, file *ast.File) ([]*Note, error) {
+ var notes []*Note
+ for _, g := range file.Comments {
+ for _, c := range g.List {
+ text, adjust := getAdjustedNote(c.Text)
+ if text == "" {
+ continue
+ }
+ parsed, err := parse(fset, token.Pos(int(c.Pos())+adjust), text)
+ if err != nil {
+ return nil, err
+ }
+ notes = append(notes, parsed...)
+ }
+ }
+ return notes, nil
+}
+
+func getAdjustedNote(text string) (string, int) {
+ if strings.HasPrefix(text, "/*") {
+ text = strings.TrimSuffix(text, "*/")
+ }
+ text = text[2:] // remove "//" or "/*" prefix
+
+ // Allow notes to appear within comments.
+ // For example:
+ // "// //@mark()" is valid.
+ // "// @mark()" is not valid.
+ // "// /*@mark()*/" is not valid.
+ var adjust int
+ if i := strings.Index(text, commentStart); i > 2 {
+ // Get the text before the commentStart.
+ pre := text[i-2 : i]
+ if pre != "//" {
+ return "", 0
+ }
+ text = text[i:]
+ adjust = i
+ }
+ if !strings.HasPrefix(text, commentStart) {
+ return "", 0
+ }
+ text = text[commentStartLen:]
+ return text, commentStartLen + adjust + 1
+}
+
const invalidToken rune = 0
type tokens struct {
diff --git a/go/expect/testdata/go.mod b/go/expect/testdata/go.mod
new file mode 100644
index 0000000..d3c4f87
--- /dev/null
+++ b/go/expect/testdata/go.mod
@@ -0,0 +1,7 @@
+module αfake1α //@mark(αMarker, "αfake1α")
+
+go 1.14
+
+require golang.org/modfile v0.0.0 //@mark(βMarker, "require golang.org/modfile v0.0.0")
+//@mark(IndirectMarker, "// indirect")
+require example.com/extramodule v1.0.0 // indirect
\ No newline at end of file
diff --git a/go/internal/cgo/cgo.go b/go/internal/cgo/cgo.go
index 0f652ea..5db8b30 100644
--- a/go/internal/cgo/cgo.go
+++ b/go/internal/cgo/cgo.go
@@ -2,9 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package cgo
-
-// This file handles cgo preprocessing of files containing `import "C"`.
+// Package cgo handles cgo preprocessing of files containing `import "C"`.
//
// DESIGN
//
@@ -51,6 +49,8 @@
// its handling of function calls, analogous to the treatment of map
// lookups in which y=m[k] and y,ok=m[k] are both legal.
+package cgo
+
import (
"fmt"
"go/ast"
diff --git a/go/internal/gccgoimporter/ar.go b/go/internal/gccgoimporter/ar.go
index 7b25abb..b8869c3 100644
--- a/go/internal/gccgoimporter/ar.go
+++ b/go/internal/gccgoimporter/ar.go
@@ -84,7 +84,7 @@
}
off += arHdrSize
- if bytes.Compare(hdrBuf[arFmagOff:arFmagOff+arFmagSize], []byte(arfmag)) != 0 {
+ if !bytes.Equal(hdrBuf[arFmagOff:arFmagOff+arFmagSize], []byte(arfmag)) {
return nil, fmt.Errorf("archive header format header (%q)", hdrBuf[:])
}
@@ -94,7 +94,7 @@
}
fn := hdrBuf[arNameOff : arNameOff+arNameSize]
- if fn[0] == '/' && (fn[1] == ' ' || fn[1] == '/' || bytes.Compare(fn[:8], []byte("/SYM64/ ")) == 0) {
+ if fn[0] == '/' && (fn[1] == ' ' || fn[1] == '/' || bytes.Equal(fn[:8], []byte("/SYM64/ "))) {
// Archive symbol table or extended name table,
// which we don't care about.
} else {
diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go
index 2a86734..29e8c60 100644
--- a/go/internal/gccgoimporter/parser.go
+++ b/go/internal/gccgoimporter/parser.go
@@ -1025,7 +1025,7 @@
func (p *parser) parseTypes(pkg *types.Package) {
maxp1 := p.parseInt()
exportedp1 := p.parseInt()
- p.typeList = make([]types.Type, maxp1, maxp1)
+ p.typeList = make([]types.Type, maxp1)
type typeOffset struct {
offset int
diff --git a/go/internal/gcimporter/gcimporter.go b/go/internal/gcimporter/gcimporter.go
index 9cf1866..8dcd8bb 100644
--- a/go/internal/gcimporter/gcimporter.go
+++ b/go/internal/gcimporter/gcimporter.go
@@ -344,7 +344,7 @@
// PackageId = string_lit .
//
-func (p *parser) parsePackageId() string {
+func (p *parser) parsePackageID() string {
id, err := strconv.Unquote(p.expect(scanner.String))
if err != nil {
p.error(err)
@@ -384,7 +384,7 @@
//
func (p *parser) parseQualifiedName() (id, name string) {
p.expect('@')
- id = p.parsePackageId()
+ id = p.parsePackageID()
p.expect('.')
// Per rev f280b8a485fd (10/2/2013), qualified names may be used for anonymous fields.
if p.tok == '?' {
@@ -696,7 +696,7 @@
// Complete requires the type's embedded interfaces to be fully defined,
// but we do not define any
- return types.NewInterface(methods, nil).Complete()
+ return newInterface(methods, nil).Complete()
}
// ChanType = ( "chan" [ "<-" ] | "<-" "chan" ) Type .
@@ -785,7 +785,7 @@
func (p *parser) parseImportDecl() {
p.expectKeyword("import")
name := p.parsePackageName()
- p.getPkg(p.parsePackageId(), name)
+ p.getPkg(p.parsePackageID(), name)
}
// int_lit = [ "+" | "-" ] { "0" ... "9" } .
diff --git a/go/internal/gcimporter/gcimporter11_test.go b/go/internal/gcimporter/gcimporter11_test.go
index 1818681..627300d 100644
--- a/go/internal/gcimporter/gcimporter11_test.go
+++ b/go/internal/gcimporter/gcimporter11_test.go
@@ -25,7 +25,7 @@
{"math.Pi", "const Pi untyped float"},
{"math.Sin", "func Sin(x float64) float64"},
{"go/ast.NotNilFilter", "func NotNilFilter(_ string, v reflect.Value) bool"},
- {"go/internal/gcimporter.BImportData", "func BImportData(fset *go/token.FileSet, imports map[string]*go/types.Package, data []byte, path string) (_ int, pkg *go/types.Package, err error)"},
+ {"go/internal/gcimporter.FindPkg", "func FindPkg(path string, srcDir string) (filename string, id string)"},
// interfaces
{"context.Context", "type Context interface{Deadline() (deadline time.Time, ok bool); Done() <-chan struct{}; Err() error; Value(key interface{}) interface{}}"},
diff --git a/go/internal/packagesdriver/sizes.go b/go/internal/packagesdriver/sizes.go
index db0c9a7..5ee692d 100644
--- a/go/internal/packagesdriver/sizes.go
+++ b/go/internal/packagesdriver/sizes.go
@@ -11,11 +11,10 @@
"encoding/json"
"fmt"
"go/types"
- "log"
- "os"
"os/exec"
"strings"
- "time"
+
+ "golang.org/x/tools/internal/gocommand"
)
var debug = false
@@ -78,97 +77,42 @@
}
func GetSizesGolist(ctx context.Context, buildFlags, env []string, dir string, usesExportData bool) (types.Sizes, error) {
- args := []string{"list", "-f", "{{context.GOARCH}} {{context.Compiler}}"}
- args = append(args, buildFlags...)
- args = append(args, "--", "unsafe")
- stdout, stderr, err := invokeGo(ctx, env, dir, usesExportData, args...)
+ inv := gocommand.Invocation{
+ Verb: "list",
+ Args: []string{"-f", "{{context.GOARCH}} {{context.Compiler}}", "--", "unsafe"},
+ Env: env,
+ BuildFlags: buildFlags,
+ WorkingDir: dir,
+ }
+ stdout, stderr, friendlyErr, rawErr := inv.RunRaw(ctx)
var goarch, compiler string
- if err != nil {
- if strings.Contains(err.Error(), "cannot find main module") {
+ if rawErr != nil {
+ if strings.Contains(rawErr.Error(), "cannot find main module") {
// User's running outside of a module. All bets are off. Get GOARCH and guess compiler is gc.
// TODO(matloob): Is this a problem in practice?
- envout, _, enverr := invokeGo(ctx, env, dir, usesExportData, "env", "GOARCH")
+ inv := gocommand.Invocation{
+ Verb: "env",
+ Args: []string{"GOARCH"},
+ Env: env,
+ WorkingDir: dir,
+ }
+ envout, enverr := inv.Run(ctx)
if enverr != nil {
- return nil, err
+ return nil, enverr
}
goarch = strings.TrimSpace(envout.String())
compiler = "gc"
} else {
- return nil, err
+ return nil, friendlyErr
}
} else {
fields := strings.Fields(stdout.String())
if len(fields) < 2 {
- return nil, fmt.Errorf("could not parse GOARCH and Go compiler in format \"<GOARCH> <compiler>\" from stdout of go command:\n%s\ndir: %s\nstdout: <<%s>>\nstderr: <<%s>>",
- cmdDebugStr(env, args...), dir, stdout.String(), stderr.String())
+ return nil, fmt.Errorf("could not parse GOARCH and Go compiler in format \"<GOARCH> <compiler>\":\nstdout: <<%s>>\nstderr: <<%s>>",
+ stdout.String(), stderr.String())
}
goarch = fields[0]
compiler = fields[1]
}
return types.SizesFor(compiler, goarch), nil
}
-
-// invokeGo returns the stdout and stderr of a go command invocation.
-func invokeGo(ctx context.Context, env []string, dir string, usesExportData bool, args ...string) (*bytes.Buffer, *bytes.Buffer, error) {
- if debug {
- defer func(start time.Time) { log.Printf("%s for %v", time.Since(start), cmdDebugStr(env, args...)) }(time.Now())
- }
- stdout := new(bytes.Buffer)
- stderr := new(bytes.Buffer)
- cmd := exec.CommandContext(ctx, "go", args...)
- // On darwin the cwd gets resolved to the real path, which breaks anything that
- // expects the working directory to keep the original path, including the
- // go command when dealing with modules.
- // The Go stdlib has a special feature where if the cwd and the PWD are the
- // same node then it trusts the PWD, so by setting it in the env for the child
- // process we fix up all the paths returned by the go command.
- cmd.Env = append(append([]string{}, env...), "PWD="+dir)
- cmd.Dir = dir
- cmd.Stdout = stdout
- cmd.Stderr = stderr
- if err := cmd.Run(); err != nil {
- exitErr, ok := err.(*exec.ExitError)
- if !ok {
- // Catastrophic error:
- // - executable not found
- // - context cancellation
- return nil, nil, fmt.Errorf("couldn't exec 'go %v': %s %T", args, err, err)
- }
-
- // Export mode entails a build.
- // If that build fails, errors appear on stderr
- // (despite the -e flag) and the Export field is blank.
- // Do not fail in that case.
- if !usesExportData {
- return nil, nil, fmt.Errorf("go %v: %s: %s", args, exitErr, stderr)
- }
- }
-
- // As of writing, go list -export prints some non-fatal compilation
- // errors to stderr, even with -e set. We would prefer that it put
- // them in the Package.Error JSON (see https://golang.org/issue/26319).
- // In the meantime, there's nowhere good to put them, but they can
- // be useful for debugging. Print them if $GOPACKAGESPRINTGOLISTERRORS
- // is set.
- if len(stderr.Bytes()) != 0 && os.Getenv("GOPACKAGESPRINTGOLISTERRORS") != "" {
- fmt.Fprintf(os.Stderr, "%s stderr: <<%s>>\n", cmdDebugStr(env, args...), stderr)
- }
-
- // debugging
- if false {
- fmt.Fprintf(os.Stderr, "%s stdout: <<%s>>\n", cmdDebugStr(env, args...), stdout)
- }
-
- return stdout, stderr, nil
-}
-
-func cmdDebugStr(envlist []string, args ...string) string {
- env := make(map[string]string)
- for _, kv := range envlist {
- split := strings.Split(kv, "=")
- k, v := split[0], split[1]
- env[k] = v
- }
-
- return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v PWD=%v go %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["PWD"], args)
-}
diff --git a/go/packages/golist.go b/go/packages/golist.go
index 3089711..b4a13ef 100644
--- a/go/packages/golist.go
+++ b/go/packages/golist.go
@@ -16,13 +16,15 @@
"path"
"path/filepath"
"reflect"
+ "sort"
"strconv"
"strings"
"sync"
- "time"
"unicode"
"golang.org/x/tools/go/internal/packagesdriver"
+ "golang.org/x/tools/internal/gocommand"
+ "golang.org/x/tools/internal/packagesinternal"
)
// debug controls verbose logging.
@@ -379,6 +381,7 @@
Imports []string
ImportMap map[string]string
Deps []string
+ Module *packagesinternal.Module
TestGoFiles []string
TestImports []string
XTestGoFiles []string
@@ -420,6 +423,8 @@
return nil, err
}
seen := make(map[string]*jsonPackage)
+ pkgs := make(map[string]*Package)
+ additionalErrors := make(map[string][]Error)
// Decode the JSON and convert it to Package form.
var response driverResponse
for dec := json.NewDecoder(buf); dec.More(); {
@@ -460,11 +465,62 @@
}
if old, found := seen[p.ImportPath]; found {
- if !reflect.DeepEqual(p, old) {
- return nil, fmt.Errorf("internal error: go list gives conflicting information for package %v", p.ImportPath)
+ // If one version of the package has an error, and the other doesn't, assume
+ // that this is a case where go list is reporting a fake dependency variant
+ // of the imported package: When a package tries to invalidly import another
+ // package, go list emits a variant of the imported package (with the same
+ // import path, but with an error on it, and the package will have a
+ // DepError set on it). An example of when this can happen is for imports of
+ // main packages: main packages can not be imported, but they may be
+ // separately matched and listed by another pattern.
+ // See golang.org/issue/36188 for more details.
+
+ // The plan is that eventually, hopefully in Go 1.15, the error will be
+ // reported on the importing package rather than the duplicate "fake"
+ // version of the imported package. Once all supported versions of Go
+ // have the new behavior this logic can be deleted.
+ // TODO(matloob): delete the workaround logic once all supported versions of
+ // Go return the errors on the proper package.
+
+ // There should be exactly one version of a package that doesn't have an
+ // error.
+ if old.Error == nil && p.Error == nil {
+ if !reflect.DeepEqual(p, old) {
+ return nil, fmt.Errorf("internal error: go list gives conflicting information for package %v", p.ImportPath)
+ }
+ continue
}
- // skip the duplicate
- continue
+
+ // Determine if this package's error needs to be bubbled up.
+ // This is a hack, and we expect for go list to eventually set the error
+ // on the package.
+ if old.Error != nil {
+ var errkind string
+ if strings.Contains(old.Error.Err, "not an importable package") {
+ errkind = "not an importable package"
+ } else if strings.Contains(old.Error.Err, "use of internal package") && strings.Contains(old.Error.Err, "not allowed") {
+ errkind = "use of internal package not allowed"
+ }
+ if errkind != "" {
+ if len(old.Error.ImportStack) < 2 {
+ return nil, fmt.Errorf(`internal error: go list gave a %q error with an import stack with fewer than two elements`, errkind)
+ }
+ importingPkg := old.Error.ImportStack[len(old.Error.ImportStack)-2]
+ additionalErrors[importingPkg] = append(additionalErrors[importingPkg], Error{
+ Pos: old.Error.Pos,
+ Msg: old.Error.Err,
+ Kind: ListError,
+ })
+ }
+ }
+
+ // Make sure that if there's a version of the package without an error,
+ // that's the one reported to the user.
+ if old.Error == nil {
+ continue
+ }
+
+ // This package will replace the old one at the end of the loop.
}
seen[p.ImportPath] = p
@@ -475,6 +531,7 @@
CompiledGoFiles: absJoin(p.Dir, p.CompiledGoFiles),
OtherFiles: absJoin(p.Dir, otherFiles(p)...),
forTest: p.ForTest,
+ module: p.Module,
}
// Work around https://golang.org/issue/28749:
@@ -563,8 +620,18 @@
})
}
+ pkgs[pkg.ID] = pkg
+ }
+
+ for id, errs := range additionalErrors {
+ if p, ok := pkgs[id]; ok {
+ p.Errors = append(p.Errors, errs...)
+ }
+ }
+ for _, pkg := range pkgs {
response.Packages = append(response.Packages, pkg)
}
+ sort.Slice(response.Packages, func(i, j int) bool { return response.Packages[i].ID < response.Packages[j].ID })
return &response, nil
}
@@ -640,29 +707,17 @@
func (state *golistState) invokeGo(verb string, args ...string) (*bytes.Buffer, error) {
cfg := state.cfg
- stdout := new(bytes.Buffer)
- stderr := new(bytes.Buffer)
- goArgs := []string{verb}
- if verb != "env" {
- goArgs = append(goArgs, cfg.BuildFlags...)
+ inv := &gocommand.Invocation{
+ Verb: verb,
+ Args: args,
+ BuildFlags: cfg.BuildFlags,
+ Env: cfg.Env,
+ Logf: cfg.Logf,
+ WorkingDir: cfg.Dir,
}
- goArgs = append(goArgs, args...)
- cmd := exec.CommandContext(state.ctx, "go", goArgs...)
- // On darwin the cwd gets resolved to the real path, which breaks anything that
- // expects the working directory to keep the original path, including the
- // go command when dealing with modules.
- // The Go stdlib has a special feature where if the cwd and the PWD are the
- // same node then it trusts the PWD, so by setting it in the env for the child
- // process we fix up all the paths returned by the go command.
- cmd.Env = append(append([]string{}, cfg.Env...), "PWD="+cfg.Dir)
- cmd.Dir = cfg.Dir
- cmd.Stdout = stdout
- cmd.Stderr = stderr
- defer func(start time.Time) {
- cfg.Logf("%s for %v, stderr: <<%s>> stdout: <<%s>>\n", time.Since(start), cmdDebugStr(cmd, goArgs...), stderr, stdout)
- }(time.Now())
- if err := cmd.Run(); err != nil {
+ stdout, stderr, _, err := inv.RunRaw(cfg.Context)
+ if err != nil {
// Check for 'go' executable not being found.
if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
return nil, fmt.Errorf("'go list' driver requires 'go', but %s", exec.ErrNotFound)
@@ -672,7 +727,7 @@
if !ok {
// Catastrophic error:
// - context cancellation
- return nil, fmt.Errorf("couldn't exec 'go %v': %s %T", args, err, err)
+ return nil, fmt.Errorf("couldn't run 'go': %v", err)
}
// Old go version?
@@ -699,7 +754,12 @@
!strings.ContainsRune("!\"#$%&'()*,:;<=>?[\\]^`{|}\uFFFD", r)
}
if len(stderr.String()) > 0 && strings.HasPrefix(stderr.String(), "# ") {
- if strings.HasPrefix(strings.TrimLeftFunc(stderr.String()[len("# "):], isPkgPathRune), "\n") {
+ msg := stderr.String()[len("# "):]
+ if strings.HasPrefix(strings.TrimLeftFunc(msg, isPkgPathRune), "\n") {
+ return stdout, nil
+ }
+ // Treat pkg-config errors as a special case (golang.org/issue/36770).
+ if strings.HasPrefix(msg, "pkg-config") {
return stdout, nil
}
}
@@ -788,16 +848,6 @@
return nil, fmt.Errorf("go %v: %s: %s", args, exitErr, stderr)
}
}
-
- // As of writing, go list -export prints some non-fatal compilation
- // errors to stderr, even with -e set. We would prefer that it put
- // them in the Package.Error JSON (see https://golang.org/issue/26319).
- // In the meantime, there's nowhere good to put them, but they can
- // be useful for debugging. Print them if $GOPACKAGESPRINTGOLISTERRORS
- // is set.
- if len(stderr.Bytes()) != 0 && os.Getenv("GOPACKAGESPRINTGOLISTERRORS") != "" {
- fmt.Fprintf(os.Stderr, "%s stderr: <<%s>>\n", cmdDebugStr(cmd, args...), stderr)
- }
return stdout, nil
}
diff --git a/go/packages/golist_overlay.go b/go/packages/golist_overlay.go
index 3760744..7974a6c 100644
--- a/go/packages/golist_overlay.go
+++ b/go/packages/golist_overlay.go
@@ -50,7 +50,6 @@
}
return overlayFiles[i] < overlayFiles[j]
})
-
for _, opath := range overlayFiles {
contents := state.cfg.Overlay[opath]
base := filepath.Base(opath)
@@ -82,14 +81,8 @@
testVariantOf = p
continue nextPackage
}
+ // We must have already seen the package of which this is a test variant.
if pkg != nil && p != pkg && pkg.PkgPath == p.PkgPath {
- // If we've already seen the test variant,
- // make sure to label which package it is a test variant of.
- if hasTestFiles(pkg) {
- testVariantOf = p
- continue nextPackage
- }
- // If we have already seen the package of which this is a test variant.
if hasTestFiles(p) {
testVariantOf = pkg
}
diff --git a/go/packages/packages.go b/go/packages/packages.go
index 586c714..1ac6558 100644
--- a/go/packages/packages.go
+++ b/go/packages/packages.go
@@ -299,12 +299,18 @@
// forTest is the package under test, if any.
forTest string
+
+ // module is the module information for the package if it exists.
+ module *packagesinternal.Module
}
func init() {
packagesinternal.GetForTest = func(p interface{}) string {
return p.(*Package).forTest
}
+ packagesinternal.GetModule = func(p interface{}) *packagesinternal.Module {
+ return p.(*Package).module
+ }
}
// An Error describes a problem with a package's metadata, syntax, or types.
diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go
index 81e0d43..0cc4379 100644
--- a/go/packages/packages_test.go
+++ b/go/packages/packages_test.go
@@ -983,10 +983,19 @@
packages.NeedImports |
packages.NeedDeps |
packages.NeedTypesSizes
- initial, err := packages.Load(exported.Config, fmt.Sprintf("file=%s", exported.File("golang.org/fake", "c/c.go")))
+ pkgs, err := packages.Load(exported.Config, fmt.Sprintf("file=%s", exported.File("golang.org/fake", "c/c.go")))
if err != nil {
t.Error(err)
}
+
+ // Find package golang.org/fake/c
+ sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].ID < pkgs[j].ID })
+ pkgc := pkgs[0]
+ if pkgc.ID != "golang.org/fake/c" {
+ t.Errorf("expected first package in sorted list to be \"golang.org/fake/c\", got %v", pkgc.ID)
+ }
+
+ // Make sure golang.org/fake/c imports net/http, as per the overlay.
contains := func(imports map[string]*packages.Package, wantImport string) bool {
for imp := range imports {
if imp == wantImport {
@@ -995,11 +1004,11 @@
}
return false
}
- for _, pkg := range initial {
- if !contains(pkg.Imports, "net/http") {
- t.Errorf("expected %s import in %s", "net/http", pkg.ID)
- }
+ if !contains(pkgc.Imports, "net/http") {
+ t.Errorf("expected import of %s in package %s, got the following imports: %v",
+ "net/http", pkgc.ID, pkgc.Imports)
}
+
}
func TestNewPackagesInOverlay(t *testing.T) { packagestest.TestAll(t, testNewPackagesInOverlay) }
@@ -1105,7 +1114,9 @@
}
// Test that we can create a package and its test package in an overlay.
-func TestOverlayNewPackageAndTest(t *testing.T) { packagestest.TestAll(t, testOverlayNewPackageAndTest) }
+func TestOverlayNewPackageAndTest(t *testing.T) {
+ packagestest.TestAll(t, testOverlayNewPackageAndTest)
+}
func testOverlayNewPackageAndTest(t *testing.T, exporter packagestest.Exporter) {
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
@@ -2409,6 +2420,38 @@
}, nil)
}
+func TestMultiplePackageVersionsIssue36188(t *testing.T) {
+ packagestest.TestAll(t, testMultiplePackageVersionsIssue36188)
+}
+
+func testMultiplePackageVersionsIssue36188(t *testing.T, exporter packagestest.Exporter) {
+ exported := packagestest.Export(t, exporter, []packagestest.Module{{
+ Name: "golang.org/fake",
+ Files: map[string]interface{}{
+ "a/a.go": `package a; import _ "golang.org/fake/b"`,
+ "b/b.go": `package main`,
+ }}})
+ pkgs, err := packages.Load(exported.Config, "golang.org/fake/a", "golang.org/fake/b")
+ if err != nil {
+ t.Fatal(err)
+ }
+ sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].ID < pkgs[j].ID })
+ if len(pkgs) != 2 {
+ t.Fatalf("expected two packages, got %v", pkgs)
+ }
+ if pkgs[0].ID != "golang.org/fake/a" && pkgs[1].ID != "golang.org/fake/b" {
+ t.Fatalf(`expected (sorted) IDs "golang.org/fake/a" and "golang.org/fake/b", got %q and %q`,
+ pkgs[0].ID, pkgs[1].ID)
+ }
+ if pkgs[0].Errors == nil {
+ t.Errorf(`expected error on package "golang.org/fake/a", got none`)
+ }
+ if pkgs[1].Errors != nil {
+ t.Errorf(`expected no errors on package "golang.org/fake/b", got %v`, pkgs[1].Errors)
+ }
+ defer exported.Cleanup()
+}
+
func TestLoadModeStrings(t *testing.T) {
testcases := []struct {
mode packages.LoadMode
diff --git a/go/packages/packagescgo_test.go b/go/packages/packagescgo_test.go
index ed51522..a149309 100644
--- a/go/packages/packagescgo_test.go
+++ b/go/packages/packagescgo_test.go
@@ -8,7 +8,10 @@
import (
"fmt"
+ "io/ioutil"
"os"
+ "os/exec"
+ "path/filepath"
"runtime"
"strings"
"testing"
@@ -66,10 +69,6 @@
}
}
-func TestCgoNoSyntax(t *testing.T) {
- packagestest.TestAll(t, testCgoNoSyntax)
-}
-
// Stolen from internal/testenv package in core.
// hasGoBuild reports whether the current system can build programs with ``go build''
// and then run them with os.StartProcess or exec.Command.
@@ -92,6 +91,9 @@
return true
}
+func TestCgoNoSyntax(t *testing.T) {
+ packagestest.TestAll(t, testCgoNoSyntax)
+}
func testCgoNoSyntax(t *testing.T, exporter packagestest.Exporter) {
// The android builders have a complex setup which causes this test to fail. See discussion on
// golang.org/cl/214943 for more details.
@@ -133,3 +135,78 @@
})
}
}
+
+func TestCgoBadPkgConfig(t *testing.T) {
+ packagestest.TestAll(t, testCgoBadPkgConfig)
+}
+func testCgoBadPkgConfig(t *testing.T, exporter packagestest.Exporter) {
+ if !hasGoBuild() {
+ t.Skip("this test can't run on platforms without go build. See discussion on golang.org/cl/214943 for more details.")
+ }
+
+ exported := packagestest.Export(t, exporter, []packagestest.Module{{
+ Name: "golang.org/fake",
+ Files: map[string]interface{}{
+ "c/c.go": `package c
+
+// #cgo pkg-config: --cflags -- foo
+import "C"`,
+ },
+ }})
+
+ dir := buildFakePkgconfig(t, exported.Config.Env)
+ defer os.RemoveAll(dir)
+ env := exported.Config.Env
+ for i, v := range env {
+ if strings.HasPrefix(v, "PATH=") {
+ env[i] = "PATH=" + dir + string(os.PathListSeparator) + v[len("PATH="):]
+ }
+ }
+
+ exported.Config.Env = append(exported.Config.Env, "CGO_ENABLED=1")
+
+ exported.Config.Mode = packages.NeedName | packages.NeedCompiledGoFiles
+ pkgs, err := packages.Load(exported.Config, "golang.org/fake/c")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(pkgs) != 1 {
+ t.Fatalf("Expected 1 package, got %v", pkgs)
+ }
+ if pkgs[0].Name != "c" {
+ t.Fatalf("Expected package to have name \"c\", got %q", pkgs[0].Name)
+ }
+}
+
+func buildFakePkgconfig(t *testing.T, env []string) string {
+ tmpdir, err := ioutil.TempDir("", "fakepkgconfig")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = ioutil.WriteFile(filepath.Join(tmpdir, "pkg-config.go"), []byte(`
+package main
+
+import "fmt"
+import "os"
+
+func main() {
+ fmt.Fprintln(os.Stderr, "bad")
+ os.Exit(2)
+}
+`), 0644)
+ if err != nil {
+ os.RemoveAll(tmpdir)
+ t.Fatal(err)
+ }
+ cmd := exec.Command("go", "build", "-o", "pkg-config", "pkg-config.go")
+ cmd.Dir = tmpdir
+ cmd.Env = env
+
+ if b, err := cmd.CombinedOutput(); err != nil {
+ os.RemoveAll(tmpdir)
+ fmt.Println(os.Environ())
+ t.Log(string(b))
+ t.Fatal(err)
+ }
+ return tmpdir
+}
diff --git a/go/packages/packagestest/expect.go b/go/packages/packagestest/expect.go
index ad769ff..ddbec44 100644
--- a/go/packages/packagestest/expect.go
+++ b/go/packages/packagestest/expect.go
@@ -7,9 +7,12 @@
import (
"fmt"
"go/token"
+ "io/ioutil"
+ "os"
"path/filepath"
"reflect"
"regexp"
+ "strings"
"golang.org/x/tools/go/expect"
"golang.org/x/tools/go/packages"
@@ -161,10 +164,47 @@
notes = append(notes, l...)
}
}
+ if _, ok := e.written[e.primary]; !ok {
+ e.notes = notes
+ return nil
+ }
+ // Check go.mod markers regardless of mode, we need to do this so that our marker count
+ // matches the counts in the summary.txt.golden file for the test directory.
+ if gomod, found := e.written[e.primary]["go.mod"]; found {
+ // If we are in Modules mode, then we need to check the contents of the go.mod.temp.
+ if e.Exporter == Modules {
+ gomod += ".temp"
+ }
+ l, err := goModMarkers(e, gomod)
+ if err != nil {
+ return fmt.Errorf("failed to extract expectations for go.mod: %v", err)
+ }
+ notes = append(notes, l...)
+ }
e.notes = notes
return nil
}
+func goModMarkers(e *Exported, gomod string) ([]*expect.Note, error) {
+ if _, err := os.Stat(gomod); os.IsNotExist(err) {
+ // If there is no go.mod file, we want to be able to continue.
+ return nil, nil
+ }
+ content, err := e.FileContents(gomod)
+ if err != nil {
+ return nil, err
+ }
+ if e.Exporter == GOPATH {
+ return expect.Parse(e.ExpectFileSet, gomod, content)
+ }
+ gomod = strings.TrimSuffix(gomod, ".temp")
+ // If we are in Modules mode, copy the original contents file back into go.mod
+ if err := ioutil.WriteFile(gomod, content, 0644); err != nil {
+ return nil, nil
+ }
+ return expect.Parse(e.ExpectFileSet, gomod, content)
+}
+
func (e *Exported) getMarkers() error {
if e.markers != nil {
return nil
diff --git a/go/packages/packagestest/export.go b/go/packages/packagestest/export.go
index 989e409..6b03926 100644
--- a/go/packages/packagestest/export.go
+++ b/go/packages/packagestest/export.go
@@ -118,11 +118,12 @@
ExpectFileSet *token.FileSet // The file set used when parsing expectations
- temp string // the temporary directory that was exported to
- primary string // the first non GOROOT module that was exported
- written map[string]map[string]string // the full set of exported files
- notes []*expect.Note // The list of expectations extracted from go source files
- markers map[string]span.Range // The set of markers extracted from go source files
+ Exporter Exporter // the exporter used
+ temp string // the temporary directory that was exported to
+ primary string // the first non GOROOT module that was exported
+ written map[string]map[string]string // the full set of exported files
+ notes []*expect.Note // The list of expectations extracted from go source files
+ markers map[string]span.Range // The set of markers extracted from go source files
}
// Exporter implementations are responsible for converting from the generic description of some
@@ -200,6 +201,7 @@
Mode: packages.LoadImports,
},
Modules: modules,
+ Exporter: exporter,
temp: temp,
primary: modules[0].Name,
written: map[string]map[string]string{},
@@ -303,6 +305,136 @@
}
}
+// GroupFilesByModules attempts to map directories to the modules within each directory.
+// This function assumes that the folder is structured in the following way:
+// - dir
+// - primarymod
+// - .go files
+// - packages
+// - go.mod (optional)
+// - modules
+// - repoa
+// - mod1
+// - .go files
+// - packages
+// - go.mod (optional)
+// It scans the directory tree anchored at root and adds a Copy writer to the
+// map for every file found.
+// This is to enable the common case in tests where you have a full copy of the
+// package in your testdata.
+func GroupFilesByModules(root string) ([]Module, error) {
+ root = filepath.FromSlash(root)
+ primarymodPath := filepath.Join(root, "primarymod")
+
+ _, err := os.Stat(primarymodPath)
+ if os.IsNotExist(err) {
+ return nil, fmt.Errorf("could not find primarymod folder within %s", root)
+ }
+
+ primarymod := &Module{
+ Name: root,
+ Files: make(map[string]interface{}),
+ Overlay: make(map[string][]byte),
+ }
+ mods := map[string]*Module{
+ root: primarymod,
+ }
+ modules := []Module{*primarymod}
+
+ if err := filepath.Walk(primarymodPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ return nil
+ }
+ fragment, err := filepath.Rel(primarymodPath, path)
+ if err != nil {
+ return err
+ }
+ primarymod.Files[filepath.ToSlash(fragment)] = Copy(path)
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+
+ modulesPath := filepath.Join(root, "modules")
+ if _, err := os.Stat(modulesPath); os.IsNotExist(err) {
+ return modules, nil
+ }
+
+ var currentRepo, currentModule string
+ updateCurrentModule := func(dir string) {
+ if dir == currentModule {
+ return
+ }
+ // Handle the case where we step into a nested directory that is a module
+ // and then step out into the parent which is also a module.
+ // Example:
+ // - repoa
+ // - moda
+ // - go.mod
+ // - v2
+ // - go.mod
+ // - what.go
+ // - modb
+ for dir != root {
+ if mods[dir] != nil {
+ currentModule = dir
+ return
+ }
+ dir = filepath.Dir(dir)
+ }
+ }
+
+ if err := filepath.Walk(modulesPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ enclosingDir := filepath.Dir(path)
+ // If the path is not a directory, then we want to add the path to
+ // the files map of the currentModule.
+ if !info.IsDir() {
+ updateCurrentModule(enclosingDir)
+ fragment, err := filepath.Rel(currentModule, path)
+ if err != nil {
+ return err
+ }
+ mods[currentModule].Files[filepath.ToSlash(fragment)] = Copy(path)
+ return nil
+ }
+ // If the path is a directory and it's enclosing folder is equal to
+ // the modules folder, then the path is a new repo.
+ if enclosingDir == modulesPath {
+ currentRepo = path
+ return nil
+ }
+ // If the path is a directory and it's enclosing folder is not the same
+ // as the current repo and it is not of the form `v1`,`v2`,...
+ // then the path is a folder/package of the current module.
+ if enclosingDir != currentRepo && !versionSuffixRE.MatchString(filepath.Base(path)) {
+ return nil
+ }
+ // If the path is a directory and it's enclosing folder is the current repo
+ // then the path is a new module.
+ module, err := filepath.Rel(modulesPath, path)
+ if err != nil {
+ return err
+ }
+ mods[path] = &Module{
+ Name: filepath.ToSlash(module),
+ Files: make(map[string]interface{}),
+ Overlay: make(map[string][]byte),
+ }
+ currentModule = path
+ modules = append(modules, *mods[path])
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ return modules, nil
+}
+
// MustCopyFileTree returns a file set for a module based on a real directory tree.
// It scans the directory tree anchored at root and adds a Copy writer to the
// map for every file found.
diff --git a/go/packages/packagestest/export_test.go b/go/packages/packagestest/export_test.go
index e2a2355..e7bf124 100644
--- a/go/packages/packagestest/export_test.go
+++ b/go/packages/packagestest/export_test.go
@@ -32,6 +32,16 @@
Files: map[string]interface{}{
"other/a.go": "package fake2",
},
+}, {
+ Name: "golang.org/fake3@v1.0.0",
+ Files: map[string]interface{}{
+ "other/a.go": "package fake3",
+ },
+}, {
+ Name: "golang.org/fake3@v1.1.0",
+ Files: map[string]interface{}{
+ "other/a.go": "package fake3",
+ },
}}
type fileTest struct {
@@ -74,3 +84,98 @@
}
}
}
+
+func TestGroupFilesByModules(t *testing.T) {
+ for _, tt := range []struct {
+ testdir string
+ want []packagestest.Module
+ }{
+ {
+ testdir: "testdata/groups/one",
+ want: []packagestest.Module{
+ {
+ Name: "testdata/groups/one",
+ Files: map[string]interface{}{
+ "main.go": true,
+ },
+ },
+ {
+ Name: "example.com/extra",
+ Files: map[string]interface{}{
+ "help.go": true,
+ },
+ },
+ },
+ },
+ {
+ testdir: "testdata/groups/two",
+ want: []packagestest.Module{
+ {
+ Name: "testdata/groups/two",
+ Files: map[string]interface{}{
+ "main.go": true,
+ "expect/yo.go": true,
+ },
+ },
+ {
+ Name: "example.com/extra",
+ Files: map[string]interface{}{
+ "yo.go": true,
+ "geez/help.go": true,
+ },
+ },
+ {
+ Name: "example.com/extra/v2",
+ Files: map[string]interface{}{
+ "me.go": true,
+ "geez/help.go": true,
+ },
+ },
+ {
+ Name: "example.com/tempmod",
+ Files: map[string]interface{}{
+ "main.go": true,
+ },
+ },
+ {
+ Name: "example.com/what@v1.0.0",
+ Files: map[string]interface{}{
+ "main.go": true,
+ },
+ },
+ {
+ Name: "example.com/what@v1.1.0",
+ Files: map[string]interface{}{
+ "main.go": true,
+ },
+ },
+ },
+ },
+ } {
+ t.Run(tt.testdir, func(t *testing.T) {
+ got, err := packagestest.GroupFilesByModules(tt.testdir)
+ if err != nil {
+ t.Fatalf("could not group files %v", err)
+ }
+ if len(got) != len(tt.want) {
+ t.Fatalf("%s: wanted %d modules but got %d", tt.testdir, len(tt.want), len(got))
+ }
+ for i, w := range tt.want {
+ g := got[i]
+ if filepath.FromSlash(g.Name) != filepath.FromSlash(w.Name) {
+ t.Fatalf("%s: wanted module[%d].Name to be %s but got %s", tt.testdir, i, filepath.FromSlash(w.Name), filepath.FromSlash(g.Name))
+ }
+ for fh := range w.Files {
+ if _, ok := g.Files[fh]; !ok {
+ t.Fatalf("%s, module[%d]: wanted %s but could not find", tt.testdir, i, fh)
+ }
+ }
+ for fh := range g.Files {
+ if _, ok := w.Files[fh]; !ok {
+ t.Fatalf("%s, module[%d]: found unexpected file %s", tt.testdir, i, fh)
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/go/packages/packagestest/modules.go b/go/packages/packagestest/modules.go
index 6d46d9b..8bf830a 100644
--- a/go/packages/packagestest/modules.go
+++ b/go/packages/packagestest/modules.go
@@ -6,16 +6,16 @@
import (
"archive/zip"
- "bytes"
+ "context"
"fmt"
"io/ioutil"
"os"
- "os/exec"
"path"
"path/filepath"
"regexp"
+ "strings"
- "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/gocommand"
)
// Modules is the exporter that produces module layouts.
@@ -40,6 +40,11 @@
type modules struct{}
+type moduleAtVersion struct {
+ module string
+ version string
+}
+
func (modules) Name() string {
return "Modules"
}
@@ -63,13 +68,45 @@
if exported.written[exported.primary] == nil {
exported.written[exported.primary] = make(map[string]string)
}
+
+ // Create a map of modulepath -> {module, version} for modulepaths
+ // that are of the form `repoa/mod1@v1.1.0`.
+ versions := make(map[string]moduleAtVersion)
+ for module := range exported.written {
+ if splt := strings.Split(module, "@"); len(splt) > 1 {
+ versions[module] = moduleAtVersion{
+ module: splt[0],
+ version: splt[1],
+ }
+ }
+ }
+
+ // If the primary module already has a go.mod, write the contents to a temp
+ // go.mod for now and then we will reset it when we are getting all the markers.
+ if gomod := exported.written[exported.primary]["go.mod"]; gomod != "" {
+ contents, err := ioutil.ReadFile(gomod)
+ if err != nil {
+ return err
+ }
+ if err := ioutil.WriteFile(gomod+".temp", contents, 0644); err != nil {
+ return err
+ }
+ }
+
exported.written[exported.primary]["go.mod"] = filepath.Join(primaryDir, "go.mod")
primaryGomod := "module " + exported.primary + "\nrequire (\n"
for other := range exported.written {
if other == exported.primary {
continue
}
- primaryGomod += fmt.Sprintf("\t%v %v\n", other, moduleVersion(other))
+ version := moduleVersion(other)
+ // If other is of the form `repo1/mod1@v1.1.0`,
+ // then we need to extract the module and the version.
+ if v, ok := versions[other]; ok {
+ other = v.module
+ version = v.version
+ }
+ primaryGomod += fmt.Sprintf("\t%v %v\n", other, version)
}
primaryGomod += ")\n"
if err := ioutil.WriteFile(filepath.Join(primaryDir, "go.mod"), []byte(primaryGomod), 0644); err != nil {
@@ -87,8 +124,12 @@
continue
}
dir := moduleDir(exported, module)
-
modfile := filepath.Join(dir, "go.mod")
+ // If other is of the form `repo1/mod1@v1.1.0`,
+ // then we need to extract the module name without the version.
+ if v, ok := versions[module]; ok {
+ module = v.module
+ }
if err := ioutil.WriteFile(modfile, []byte("module "+module+"\n"), 0644); err != nil {
return err
}
@@ -101,9 +142,15 @@
if module == exported.primary {
continue
}
+ version := moduleVersion(module)
+ // If other is of the form `repo1/mod1@v1.1.0`,
+ // then we need to extract the module and the version.
+ if v, ok := versions[module]; ok {
+ module = v.module
+ version = v.version
+ }
dir := filepath.Join(proxyDir, module, "@v")
-
- if err := writeModuleProxy(dir, module, files); err != nil {
+ if err := writeModuleProxy(dir, module, version, files); err != nil {
return fmt.Errorf("creating module proxy dir for %v: %v", module, err)
}
}
@@ -122,22 +169,33 @@
// Run go mod download to recreate the mod cache dir with all the extra
// stuff in cache. All the files created by Export should be recreated.
- if err := invokeGo(exported.Config, "mod", "download"); err != nil {
+ inv := gocommand.Invocation{
+ Verb: "mod",
+ Args: []string{"download"},
+ Env: exported.Config.Env,
+ BuildFlags: exported.Config.BuildFlags,
+ WorkingDir: exported.Config.Dir,
+ }
+ if _, err := inv.Run(context.Background()); err != nil {
return err
}
-
return nil
}
// writeModuleProxy creates a directory in the proxy dir for a module.
-func writeModuleProxy(dir, module string, files map[string]string) error {
- ver := moduleVersion(module)
+func writeModuleProxy(dir, module, ver string, files map[string]string) error {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
- // list file. Just the single version.
- if err := ioutil.WriteFile(filepath.Join(dir, "list"), []byte(ver+"\n"), 0644); err != nil {
+ // the modproxy checks for versions by looking at the "list" file,
+ // since we are supporting multiple versions, create the file if it does not exist or
+ // append the version number to the preexisting file.
+ f, err := os.OpenFile(filepath.Join(dir, "list"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ return err
+ }
+ if _, err := f.WriteString(ver + "\n"); err != nil {
return err
}
@@ -157,7 +215,7 @@
}
// zip of all the source files.
- f, err := os.OpenFile(filepath.Join(dir, ver+".zip"), os.O_CREATE|os.O_WRONLY, 0644)
+ f, err = os.OpenFile(filepath.Join(dir, ver+".zip"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
@@ -185,29 +243,18 @@
return nil
}
-func invokeGo(cfg *packages.Config, args ...string) error {
- stdout := new(bytes.Buffer)
- stderr := new(bytes.Buffer)
- cmd := exec.Command("go", args...)
- cmd.Env = append(append([]string{}, cfg.Env...), "PWD="+cfg.Dir)
- cmd.Dir = cfg.Dir
- cmd.Stdout = stdout
- cmd.Stderr = stderr
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("go %v: %s: %s", args, err, stderr)
- }
- return nil
-}
-
func modCache(exported *Exported) string {
return filepath.Join(exported.temp, "modcache/pkg/mod")
}
func primaryDir(exported *Exported) string {
- return filepath.Join(exported.temp, "primarymod", path.Base(exported.primary))
+ return filepath.Join(exported.temp, path.Base(exported.primary))
}
func moduleDir(exported *Exported, module string) string {
+ if strings.Contains(module, "@") {
+ return filepath.Join(modCache(exported), module)
+ }
return filepath.Join(modCache(exported), path.Dir(module), path.Base(module)+"@"+moduleVersion(module))
}
diff --git a/go/packages/packagestest/modules_test.go b/go/packages/packagestest/modules_test.go
index 8a85ae1..5d13176 100644
--- a/go/packages/packagestest/modules_test.go
+++ b/go/packages/packagestest/modules_test.go
@@ -17,16 +17,18 @@
exported := packagestest.Export(t, packagestest.Modules, testdata)
defer exported.Cleanup()
// Check that the cfg contains all the right bits
- var expectDir = filepath.Join(exported.Temp(), "primarymod/fake1")
+ var expectDir = filepath.Join(exported.Temp(), "fake1")
if exported.Config.Dir != expectDir {
t.Errorf("Got working directory %v expected %v", exported.Config.Dir, expectDir)
}
checkFiles(t, exported, []fileTest{
- {"golang.org/fake1", "go.mod", "primarymod/fake1/go.mod", nil},
- {"golang.org/fake1", "a.go", "primarymod/fake1/a.go", checkLink("testdata/a.go")},
- {"golang.org/fake1", "b.go", "primarymod/fake1/b.go", checkContent("package fake1")},
+ {"golang.org/fake1", "go.mod", "fake1/go.mod", nil},
+ {"golang.org/fake1", "a.go", "fake1/a.go", checkLink("testdata/a.go")},
+ {"golang.org/fake1", "b.go", "fake1/b.go", checkContent("package fake1")},
{"golang.org/fake2", "go.mod", "modcache/pkg/mod/golang.org/fake2@v1.0.0/go.mod", nil},
{"golang.org/fake2", "other/a.go", "modcache/pkg/mod/golang.org/fake2@v1.0.0/other/a.go", checkContent("package fake2")},
{"golang.org/fake2/v2", "other/a.go", "modcache/pkg/mod/golang.org/fake2/v2@v2.0.0/other/a.go", checkContent("package fake2")},
+ {"golang.org/fake3@v1.1.0", "other/a.go", "modcache/pkg/mod/golang.org/fake3@v1.1.0/other/a.go", checkContent("package fake3")},
+ {"golang.org/fake3@v1.0.0", "other/a.go", "modcache/pkg/mod/golang.org/fake3@v1.0.0/other/a.go", nil},
})
}
diff --git a/go/packages/packagestest/testdata/groups/one/modules/example.com/extra/help.go b/go/packages/packagestest/testdata/groups/one/modules/example.com/extra/help.go
new file mode 100644
index 0000000..ee03293
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/one/modules/example.com/extra/help.go
@@ -0,0 +1 @@
+package extra
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/one/primarymod/main.go b/go/packages/packagestest/testdata/groups/one/primarymod/main.go
new file mode 100644
index 0000000..54fe6e8
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/one/primarymod/main.go
@@ -0,0 +1 @@
+package one
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/geez/help.go b/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/geez/help.go
new file mode 100644
index 0000000..930ffdc
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/geez/help.go
@@ -0,0 +1 @@
+package example.com/extra/geez
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/v2/geez/help.go b/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/v2/geez/help.go
new file mode 100644
index 0000000..930ffdc
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/v2/geez/help.go
@@ -0,0 +1 @@
+package example.com/extra/geez
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/v2/me.go b/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/v2/me.go
new file mode 100644
index 0000000..6a8c7d3
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/v2/me.go
@@ -0,0 +1 @@
+package example.com/extra
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/yo.go b/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/yo.go
new file mode 100644
index 0000000..6a8c7d3
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/two/modules/example.com/extra/yo.go
@@ -0,0 +1 @@
+package example.com/extra
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/two/modules/example.com/tempmod/main.go b/go/packages/packagestest/testdata/groups/two/modules/example.com/tempmod/main.go
new file mode 100644
index 0000000..85dbfa7
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/two/modules/example.com/tempmod/main.go
@@ -0,0 +1 @@
+package example.com/tempmod
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/two/modules/example.com/what@v1.0.0/main.go b/go/packages/packagestest/testdata/groups/two/modules/example.com/what@v1.0.0/main.go
new file mode 100644
index 0000000..4723ee6
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/two/modules/example.com/what@v1.0.0/main.go
@@ -0,0 +1 @@
+package example.com/what
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/two/modules/example.com/what@v1.1.0/main.go b/go/packages/packagestest/testdata/groups/two/modules/example.com/what@v1.1.0/main.go
new file mode 100644
index 0000000..4723ee6
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/two/modules/example.com/what@v1.1.0/main.go
@@ -0,0 +1 @@
+package example.com/what
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/two/primarymod/expect/yo.go b/go/packages/packagestest/testdata/groups/two/primarymod/expect/yo.go
new file mode 100644
index 0000000..a706778
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/two/primarymod/expect/yo.go
@@ -0,0 +1 @@
+package expect
\ No newline at end of file
diff --git a/go/packages/packagestest/testdata/groups/two/primarymod/main.go b/go/packages/packagestest/testdata/groups/two/primarymod/main.go
new file mode 100644
index 0000000..0b26334
--- /dev/null
+++ b/go/packages/packagestest/testdata/groups/two/primarymod/main.go
@@ -0,0 +1 @@
+package two
\ No newline at end of file
diff --git a/go/pointer/api.go b/go/pointer/api.go
index 3c5c6dc..2a13a67 100644
--- a/go/pointer/api.go
+++ b/go/pointer/api.go
@@ -230,11 +230,11 @@
if s.pts != nil {
var space [50]int
for _, x := range s.pts.AppendTo(space[:0]) {
- ifaceObjId := nodeid(x)
- if !s.a.isTaggedObject(ifaceObjId) {
+ ifaceObjID := nodeid(x)
+ if !s.a.isTaggedObject(ifaceObjID) {
continue // !CanHaveDynamicTypes(tDyn)
}
- tDyn, v, indirect := s.a.taggedValue(ifaceObjId)
+ tDyn, v, indirect := s.a.taggedValue(ifaceObjID)
if indirect {
panic("indirect tagged object") // implement later
}
@@ -251,13 +251,13 @@
// Intersects reports whether this points-to set and the
// argument points-to set contain common members.
-func (x PointsToSet) Intersects(y PointsToSet) bool {
- if x.pts == nil || y.pts == nil {
+func (s PointsToSet) Intersects(y PointsToSet) bool {
+ if s.pts == nil || y.pts == nil {
return false
}
// This takes Θ(|x|+|y|) time.
var z intsets.Sparse
- z.Intersection(&x.pts.Sparse, &y.pts.Sparse)
+ z.Intersection(&s.pts.Sparse, &y.pts.Sparse)
return !z.IsEmpty()
}
diff --git a/go/pointer/gen.go b/go/pointer/gen.go
index f2a5171..5d2d621 100644
--- a/go/pointer/gen.go
+++ b/go/pointer/gen.go
@@ -20,7 +20,7 @@
)
var (
- tEface = types.NewInterface(nil, nil).Complete()
+ tEface = types.NewInterfaceType(nil, nil).Complete()
tInvalid = types.Typ[types.Invalid]
tUnsafePtr = types.Typ[types.UnsafePointer]
)
@@ -503,8 +503,7 @@
y := instr.Call.Args[1]
tArray := sliceToArray(instr.Call.Args[0].Type())
- var w nodeid
- w = a.nextNode()
+ w := a.nextNode()
a.addNodes(tArray, "append")
a.endObject(w, cgn, instr)
diff --git a/go/pointer/hvn.go b/go/pointer/hvn.go
index 192e405..52fd479 100644
--- a/go/pointer/hvn.go
+++ b/go/pointer/hvn.go
@@ -391,10 +391,9 @@
if debugHVNVerbose && h.log != nil {
fmt.Fprintf(h.log, "\to%d --> o%d\n", h.ref(odst), osrc)
}
- } else {
- // We don't interpret store-with-offset.
- // See discussion of soundness at markIndirectNodes.
}
+ // We don't interpret store-with-offset.
+ // See discussion of soundness at markIndirectNodes.
}
// dst = &src.offset
@@ -785,11 +784,11 @@
assert(peLabels.Len() == 1, "PE class is not a singleton")
label := peLabel(peLabels.Min())
- canonId := canon[label]
- if canonId == nodeid(h.N) {
+ canonID := canon[label]
+ if canonID == nodeid(h.N) {
// id becomes the representative of the PE label.
- canonId = id
- canon[label] = canonId
+ canonID = id
+ canon[label] = canonID
if h.a.log != nil {
fmt.Fprintf(h.a.log, "\tpts(n%d) is canonical : \t(%s)\n",
@@ -798,8 +797,8 @@
} else {
// Link the solver states for the two nodes.
- assert(h.a.nodes[canonId].solve != nil, "missing solver state")
- h.a.nodes[id].solve = h.a.nodes[canonId].solve
+ assert(h.a.nodes[canonID].solve != nil, "missing solver state")
+ h.a.nodes[id].solve = h.a.nodes[canonID].solve
if h.a.log != nil {
// TODO(adonovan): debug: reorganize the log so it prints
@@ -807,11 +806,11 @@
// pe y = x1, ..., xn
// for each canonical y. Requires allocation.
fmt.Fprintf(h.a.log, "\tpts(n%d) = pts(n%d) : %s\n",
- id, canonId, h.a.nodes[id].typ)
+ id, canonID, h.a.nodes[id].typ)
}
}
- mapping[id] = canonId
+ mapping[id] = canonID
}
// Renumber the constraints, eliminate duplicates, and eliminate
diff --git a/go/pointer/opt.go b/go/pointer/opt.go
index 81f80aa..6defea1 100644
--- a/go/pointer/opt.go
+++ b/go/pointer/opt.go
@@ -34,8 +34,8 @@
}
N := nodeid(len(a.nodes))
- newNodes := make([]*node, N, N)
- renumbering := make([]nodeid, N, N) // maps old to new
+ newNodes := make([]*node, N)
+ renumbering := make([]nodeid, N) // maps old to new
var i, j nodeid
diff --git a/go/pointer/util.go b/go/pointer/util.go
index 683fddd..986dd09 100644
--- a/go/pointer/util.go
+++ b/go/pointer/util.go
@@ -277,8 +277,8 @@
return ns.Sparse.Insert(int(n))
}
-func (x *nodeset) addAll(y *nodeset) bool {
- return x.UnionWith(&y.Sparse)
+func (ns *nodeset) addAll(y *nodeset) bool {
+ return ns.UnionWith(&y.Sparse)
}
// Profiling & debugging -------------------------------------------------------
diff --git a/go/ssa/dom.go b/go/ssa/dom.go
index 12ef430..822fe97 100644
--- a/go/ssa/dom.go
+++ b/go/ssa/dom.go
@@ -53,7 +53,7 @@
//
func (f *Function) DomPreorder() []*BasicBlock {
n := len(f.Blocks)
- order := make(byDomPreorder, n, n)
+ order := make(byDomPreorder, n)
copy(order, f.Blocks)
sort.Sort(order)
return order
@@ -123,7 +123,7 @@
n := len(f.Blocks)
// Allocate space for 5 contiguous [n]*BasicBlock arrays:
// sdom, parent, ancestor, preorder, buckets.
- space := make([]*BasicBlock, 5*n, 5*n)
+ space := make([]*BasicBlock, 5*n)
lt := ltState{
sdom: space[0:n],
parent: space[n : 2*n],
diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go
index a216ac7..28ebf5f 100644
--- a/go/ssa/interp/interp_test.go
+++ b/go/ssa/interp/interp_test.go
@@ -23,7 +23,6 @@
"log"
"os"
"path/filepath"
- "runtime"
"strings"
"testing"
"time"
@@ -125,10 +124,10 @@
}
func run(t *testing.T, input string) bool {
- // The recover2 test case is broken when run against tip. See golang/go#34089.
- // TODO(matloob): Figure out what's going on or fix this before go1.14 is released.
- if filepath.Base(input) == "recover2.go" && strings.HasPrefix(runtime.Version(), "devel") {
- t.Skip("The recover2.go test is broken in tip. See golang.org/issue/34089.")
+ // The recover2 test case is broken on Go 1.14+. See golang/go#34089.
+ // TODO(matloob): Fix this.
+ if filepath.Base(input) == "recover2.go" {
+ t.Skip("The recover2.go test is broken in go1.14+. See golang.org/issue/34089.")
}
t.Logf("Input: %s\n", input)
diff --git a/go/ssa/source_test.go b/go/ssa/source_test.go
index 9dc3c66..24cf57e 100644
--- a/go/ssa/source_test.go
+++ b/go/ssa/source_test.go
@@ -50,7 +50,7 @@
// Each note of the form @ssa(x, "BinOp") in testdata/objlookup.go
// specifies an expectation that an object named x declared on the
// same line is associated with an an ssa.Value of type *ssa.BinOp.
- notes, err := expect.Extract(conf.Fset, f)
+ notes, err := expect.ExtractGo(conf.Fset, f)
if err != nil {
t.Fatal(err)
}
@@ -271,7 +271,7 @@
return true
})
- notes, err := expect.Extract(prog.Fset, f)
+ notes, err := expect.ExtractGo(prog.Fset, f)
if err != nil {
t.Fatal(err)
}
diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go
index 78272c5..4dfdafd 100644
--- a/go/ssa/ssa.go
+++ b/go/ssa/ssa.go
@@ -1478,6 +1478,8 @@
func (c *NamedConst) Package() *Package { return c.pkg }
func (c *NamedConst) RelString(from *types.Package) string { return relString(c, from) }
+func (d *DebugRef) Object() types.Object { return d.object }
+
// Func returns the package-level function of the specified name,
// or nil if not found.
//
diff --git a/go/types/typeutil/callee_test.go b/go/types/typeutil/callee_test.go
index a0d107d..272e1eb 100644
--- a/go/types/typeutil/callee_test.go
+++ b/go/types/typeutil/callee_test.go
@@ -63,7 +63,7 @@
Uses: make(map[*ast.Ident]types.Object),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
}
- cfg := &types.Config{Importer: importer.For("source", nil)}
+ cfg := &types.Config{Importer: importer.ForCompiler(fset, "source", nil)}
if _, err := cfg.Check("p", fset, []*ast.File{f}, info); err != nil {
t.Fatal(err)
}
diff --git a/go/types/typeutil/map_test.go b/go/types/typeutil/map_test.go
index 34facbe..d4b0f63 100644
--- a/go/types/typeutil/map_test.go
+++ b/go/types/typeutil/map_test.go
@@ -47,7 +47,7 @@
tmap.At(tPStr1)
tmap.Delete(tPStr1)
tmap.KeysString()
- tmap.String()
+ _ = tmap.String()
tmap = new(typeutil.Map)
diff --git a/godoc/dirtrees.go b/godoc/dirtrees.go
index e9483a0..82c9a06 100644
--- a/godoc/dirtrees.go
+++ b/godoc/dirtrees.go
@@ -332,8 +332,8 @@
// If filter is set, only the directory entries whose paths match the filter
// are included.
//
-func (root *Directory) listing(skipRoot bool, filter func(string) bool) *DirList {
- if root == nil {
+func (dir *Directory) listing(skipRoot bool, filter func(string) bool) *DirList {
+ if dir == nil {
return nil
}
@@ -341,7 +341,7 @@
n := 0
minDepth := 1 << 30 // infinity
maxDepth := 0
- for d := range root.iter(skipRoot) {
+ for d := range dir.iter(skipRoot) {
n++
if minDepth > d.Depth {
minDepth = d.Depth
@@ -358,7 +358,7 @@
// create list
list := make([]DirEntry, 0, n)
- for d := range root.iter(skipRoot) {
+ for d := range dir.iter(skipRoot) {
if filter != nil && !filter(d.Path) {
continue
}
@@ -368,7 +368,7 @@
// the path is relative to root.Path - remove the root.Path
// prefix (the prefix should always be present but avoid
// crashes and check)
- path := strings.TrimPrefix(d.Path, root.Path)
+ path := strings.TrimPrefix(d.Path, dir.Path)
// remove leading separator if any - path must be relative
path = strings.TrimPrefix(path, "/")
p.Path = path
diff --git a/godoc/godoc.go b/godoc/godoc.go
index 810da32..6d4d115 100644
--- a/godoc/godoc.go
+++ b/godoc/godoc.go
@@ -312,9 +312,7 @@
//
// TODO: do this better, so it works for all
// comments, including unconventional ones.
- if bytes.HasPrefix(line, commentPrefix) {
- line = line[len(commentPrefix):]
- }
+ line = bytes.TrimPrefix(line, commentPrefix)
id := scanIdentifier(line)
if len(id) == 0 {
// No leading identifier. Avoid map lookup for
diff --git a/godoc/redirect/redirect.go b/godoc/redirect/redirect.go
index b4599f6..f0e8a10 100644
--- a/godoc/redirect/redirect.go
+++ b/godoc/redirect/redirect.go
@@ -156,7 +156,7 @@
})
}
-var validId = regexp.MustCompile(`^[A-Za-z0-9-]*/?$`)
+var validID = regexp.MustCompile(`^[A-Za-z0-9-]*/?$`)
func PrefixHandler(prefix, baseURL string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -166,7 +166,7 @@
return
}
id := r.URL.Path[len(prefix):]
- if !validId.MatchString(id) {
+ if !validID.MatchString(id) {
http.Error(w, "Not found", http.StatusNotFound)
return
}
@@ -192,7 +192,7 @@
id := r.URL.Path[len(prefix):]
// support /cl/152700045/, which is used in commit 0edafefc36.
id = strings.TrimSuffix(id, "/")
- if !validId.MatchString(id) {
+ if !validID.MatchString(id) {
http.Error(w, "Not found", http.StatusNotFound)
return
}
diff --git a/godoc/static/gen_test.go b/godoc/static/gen_test.go
index 1889b16..7f74329 100644
--- a/godoc/static/gen_test.go
+++ b/godoc/static/gen_test.go
@@ -27,7 +27,7 @@
t.Errorf("error while generating static.go: %v\n", err)
}
- if bytes.Compare(oldBuf, newBuf) != 0 {
+ if !bytes.Equal(oldBuf, newBuf) {
t.Error(`static.go is stale. Run:
$ go generate golang.org/x/tools/godoc/static
$ git diff
diff --git a/gopls/doc/status.md b/gopls/doc/status.md
index 740a09d..435b9ec 100644
--- a/gopls/doc/status.md
+++ b/gopls/doc/status.md
@@ -21,26 +21,17 @@
## Known issues
-1. Cursor resets to the beginning or end of file on format: [#31937]
1. Editing multiple modules in one editor window: [#32394]
-1. Language features do not work with cgo: [#32898]
+1. Type checking does not work in cgo packages: [#35721]
1. Does not work with build tags: [#29202]
-1. Find references and rename only work in a single package: [#32869], [#32877]
-1. Completion does not work well after go or defer statements: [#29313]
-1. Changes in files outside of the editor are not yet tracked: [#31553]
+1. Find references and rename only work in a single package: [#32877]
[x/tools]: https://github.com/golang/tools
[golang.org/x/tools/gopls]: https://github.com/golang/tools/tree/master/gopls
[golang.org/x/tools/internal/lsp]: https://github.com/golang/tools/tree/master/internal/lsp
-[#31937]: https://github.com/golang/go/issues/31937
[#32394]: https://github.com/golang/go/issues/32394
-[#32898]: https://github.com/golang/go/issues/32898
+[#35721]: https://github.com/golang/go/issues/35721
[#29202]: https://github.com/golang/go/issues/29202
-[#32869]: https://github.com/golang/go/issues/32869
[#32877]: https://github.com/golang/go/issues/32877
-[#29313]: https://github.com/golang/go/issues/29313
-[#31553]: https://github.com/golang/go/issues/31553
-
-
diff --git a/gopls/doc/vscode.md b/gopls/doc/vscode.md
index 3e834e2..f05c61b 100644
--- a/gopls/doc/vscode.md
+++ b/gopls/doc/vscode.md
@@ -5,18 +5,20 @@
```json5
"go.useLanguageServer": true,
"[go]": {
- "editor.snippetSuggestions": "none",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
- }
+ },
+ // Optional: Disable snippets, as they conflict with completion ranking.
+ "editor.snippetSuggestions": "none",
},
"gopls": {
- "usePlaceholders": true, // add parameter placeholders when completing a function
+ // Add parameter placeholders when completing a function.
+ "usePlaceholders": true,
- // Experimental settings
- "completeUnimported": true, // autocomplete unimported packages
- "deepCompletion": true, // enable deep completion
+ // If true, enable additional analyses with staticcheck.
+ // Warning: This will significantly increase memory usage.
+ "staticcheck": false,
}
```
diff --git a/gopls/go.mod b/gopls/go.mod
index 8496bca..1cf9b16 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -6,7 +6,7 @@
github.com/sergi/go-diff v1.0.0
github.com/stretchr/testify v1.4.0 // indirect
golang.org/x/tools v0.0.0-20200212213342-7a21e308cf6c
- honnef.co/go/tools v0.0.1-2019.2.3
+ honnef.co/go/tools v0.0.1-2020.1.3
mvdan.cc/xurls/v2 v2.1.0
)
diff --git a/gopls/go.sum b/gopls/go.sum
index f935705..0d1b780 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -38,7 +38,7 @@
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
mvdan.cc/xurls/v2 v2.1.0 h1:KaMb5GLhlcSX+e+qhbRJODnUUBvlw01jt4yrjFIHAuA=
mvdan.cc/xurls/v2 v2.1.0/go.mod h1:5GrSd9rOnKOpZaji1OZLYL/yeAAtGDlo/cFe+8K5n8E=
diff --git a/gopls/integration/govim/Dockerfile b/gopls/integration/govim/Dockerfile
index 0ec78a8..614d7fe 100644
--- a/gopls/integration/govim/Dockerfile
+++ b/gopls/integration/govim/Dockerfile
@@ -10,15 +10,11 @@
# that we're not sensitive to test breakages in govim.
# TODO(findleyr): Once a version of govim has been tagged containing
# https://github.com/govim/govim/pull/629, switch this to @latest.
-ENV GOPROXY=https://proxy.golang.org GOPATH=/go VIM_FLAVOR=vim \
- GOVIM_VERSION=v0.0.27-0.20200122085353-96e61d22452d
+ENV GOPROXY=https://proxy.golang.org GOPATH=/go VIM_FLAVOR=vim
WORKDIR /src
-# Get govim, as well as all dependencies for the ./cmd/govim tests. In order to
-# use the go command's module version resolution, we use `go mod download` for
-# this rather than git. Another approach would be to use a temporary module and
-# `go get -t -u github.com/govim/govim`, but that may cause test dependencies
-# to be upgraded.
-RUN GOVIM_DIR=$(go mod download -json github.com/govim/govim@$GOVIM_VERSION | jq -r .Dir) && \
- cp -r $GOVIM_DIR /src/govim && cd /src/govim && \
- go list -test -deps ./cmd/govim
+# Clone govim. In order to use the go command for resolving latest, we download
+# a redundant copy of govim to the build cache using `go mod download`.
+RUN GOVIM_VERSION=$(go mod download -json github.com/govim/govim@latest | jq -r .Version) && \
+ git clone https://github.com/govim/govim /src/govim && cd /src/govim && \
+ git checkout $GOVIM_VERSION
diff --git a/gopls/integration/govim/README.md b/gopls/integration/govim/README.md
index c3ae925..da3a98b 100644
--- a/gopls/integration/govim/README.md
+++ b/gopls/integration/govim/README.md
@@ -6,25 +6,36 @@
## Running on GCP
To run these integration tests in Cloud Build, use the following steps. Here
-we assume that `$PROJECT` is a valid GCP project and `$BUCKET` is a cloud
+we assume that `$PROJECT_ID` is a valid GCP project and `$BUCKET` is a cloud
storage bucket owned by that project.
- `cd` to the root directory of the tools project.
- (at least once per GCP project) Build the test harness:
```
$ gcloud builds submit \
- --project="${PROJECT}" \
+ --project="${PROJECT_ID}" \
--config=gopls/integration/govim/cloudbuild.harness.yaml \
--substitutions=_RESULT_BUCKET="${BUCKET}"
```
- Run the integration tests:
```
$ gcloud builds submit \
- --project="${PROJECT}" \
+ --project="${PROJECT_ID}" \
--config=gopls/integration/govim/cloudbuild.yaml \
--substitutions=_RESULT_BUCKET="${BUCKET}"
```
+## Fetching Artifacts
+
+Assuming the artifacts bucket is world readable, you can fetch integration from
+GCS. They are located at:
+
+- logs: `https://storage.googleapis.com/${BUCKET}/log-${EVALUATION_ID}.txt`
+- artifact tarball: `https://storage.googleapis.com/${BUCKET}/govim/${EVALUATION_ID}/artifacts.tar.gz`
+
+The `artifacts.go` command can be used to fetch both artifacts using an
+evaluation id.
+
## Running locally
Run `gopls/integration/govim/run_local.sh`. This may take a while the first
diff --git a/gopls/integration/govim/artifacts.go b/gopls/integration/govim/artifacts.go
new file mode 100644
index 0000000..40a8547
--- /dev/null
+++ b/gopls/integration/govim/artifacts.go
@@ -0,0 +1,67 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path"
+)
+
+var bucket = flag.String("bucket", "golang-gopls_integration_tests", "GCS bucket holding test artifacts.")
+
+const usage = `
+artifacts [--bucket=<bucket ID>] <cloud build evaluation ID>
+
+Fetch artifacts from an integration test run. Evaluation ID should be extracted
+from the cloud build notification.
+
+In order for this to work, the GCS bucket that artifacts were written to must
+be publicly readable. By default, this fetches from the
+golang-gopls_integration_tests bucket.
+`
+
+func main() {
+ flag.Usage = func() {
+ fmt.Fprintf(flag.CommandLine.Output(), usage)
+ }
+ flag.Parse()
+ if flag.NArg() != 1 {
+ flag.Usage()
+ os.Exit(2)
+ }
+ evalID := flag.Arg(0)
+ logURL := fmt.Sprintf("https://storage.googleapis.com/%s/log-%s.txt", *bucket, evalID)
+ if err := download(logURL); err != nil {
+ fmt.Fprintf(os.Stderr, "downloading logs: %v", err)
+ }
+ tarURL := fmt.Sprintf("https://storage.googleapis.com/%s/govim/%s/artifacts.tar.gz", *bucket, evalID)
+ if err := download(tarURL); err != nil {
+ fmt.Fprintf(os.Stderr, "downloading artifact tarball: %v", err)
+ }
+}
+
+func download(artifactURL string) error {
+ name := path.Base(artifactURL)
+ resp, err := http.Get(artifactURL)
+ if err != nil {
+ return fmt.Errorf("fetching from GCS: %v", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("got status code %d from GCS", resp.StatusCode)
+ }
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("reading result: %v", err)
+ }
+ if err := ioutil.WriteFile(name, data, 0644); err != nil {
+ return fmt.Errorf("writing artifact: %v", err)
+ }
+ return nil
+}
diff --git a/gopls/integration/govim/cloudbuild.harness.yaml b/gopls/integration/govim/cloudbuild.harness.yaml
index 0deb8af..13b0a34 100644
--- a/gopls/integration/govim/cloudbuild.harness.yaml
+++ b/gopls/integration/govim/cloudbuild.harness.yaml
@@ -10,7 +10,7 @@
# To allow for breaking changes to this test harness, tag with a major
# version number.
'-t', 'gcr.io/$PROJECT_ID/govim-harness:latest',
- '-t', 'gcr.io/$PROJECT_ID/govim-harness:2',
+ '-t', 'gcr.io/$PROJECT_ID/govim-harness:3',
# It is assumed that this build is running from the root directory of the
# tools repository.
'-f', 'gopls/integration/govim/Dockerfile',
diff --git a/gopls/integration/govim/cloudbuild.yaml b/gopls/integration/govim/cloudbuild.yaml
index a19c787..f0b982a 100644
--- a/gopls/integration/govim/cloudbuild.yaml
+++ b/gopls/integration/govim/cloudbuild.yaml
@@ -20,7 +20,7 @@
# Run the tests. Note that the script in this step does not return the exit
# code from `go test`, but rather saves it for use in the final step after
# uploading artifacts.
- - name: 'gcr.io/$PROJECT_ID/govim-harness:2'
+ - name: 'gcr.io/$PROJECT_ID/govim-harness:3'
dir: '/src/govim'
env:
- GOVIM_TESTSCRIPT_WORKDIR_ROOT=/workspace/artifacts
diff --git a/gopls/integration/govim/run_tests_for_cloudbuild.sh b/gopls/integration/govim/run_tests_for_cloudbuild.sh
index 47a9cfd..0f59938 100755
--- a/gopls/integration/govim/run_tests_for_cloudbuild.sh
+++ b/gopls/integration/govim/run_tests_for_cloudbuild.sh
@@ -14,3 +14,15 @@
# Stash the error, for use in a later build step.
echo "exit $?" > /workspace/govim_test_result.sh
+
+# Clean up unnecessary artifacts. This is based on govim/_scripts/tidyUp.bash.
+# Since we're fetching govim using the go command, we won't have this non-go
+# source directory available to us.
+if [[ -n "$GOVIM_TESTSCRIPT_WORKDIR_ROOT" ]]; then
+ echo "Cleaning up build artifacts..."
+ # Make artifacts writable so that rm -rf doesn't complain.
+ chmod -R u+w "$GOVIM_TESTSCRIPT_WORKDIR_ROOT"
+
+ # Remove directories we don't care about.
+ find "$GOVIM_TESTSCRIPT_WORKDIR_ROOT" -type d \( -name .vim -o -name gopath \) -prune -exec rm -rf '{}' \;
+fi
diff --git a/gopls/integration/replay/main.go b/gopls/integration/replay/main.go
index cd30dd6..1bd69ac 100644
--- a/gopls/integration/replay/main.go
+++ b/gopls/integration/replay/main.go
@@ -68,7 +68,7 @@
log.Printf("new %d, hist:%s", len(newMsgs), seen.Histogram)
ok := make(map[string]int)
- f := func(x []*parse.Logmsg, label string, diags map[string][]p.Diagnostic) {
+ f := func(x []*parse.Logmsg, label string, diags map[p.DocumentURI][]p.Diagnostic) {
counts := make(map[parse.MsgType]int)
for _, l := range x {
if l.Method == "window/logMessage" {
@@ -102,9 +102,9 @@
}
log.Printf("%s: %s", label, msg)
}
- mdiags := make(map[string][]p.Diagnostic)
+ mdiags := make(map[p.DocumentURI][]p.Diagnostic)
f(msgs, "old", mdiags)
- vdiags := make(map[string][]p.Diagnostic)
+ vdiags := make(map[p.DocumentURI][]p.Diagnostic)
f(newMsgs, "new", vdiags)
buf := []string{}
for k := range ok {
diff --git a/gopls/main.go b/gopls/main.go
index 0b4d889..0de6531 100644
--- a/gopls/main.go
+++ b/gopls/main.go
@@ -6,6 +6,9 @@
// The Language Server Protocol allows any text editor
// to be extended with IDE-like features;
// see https://langserver.org/ for details.
+//
+// See https://github.com/golang/tools/tree/master/gopls
+// for the most up-to-date information on the gopls status.
package main // import "golang.org/x/tools/gopls"
import (
diff --git a/gopls/test/gopls_test.go b/gopls/test/gopls_test.go
index d57a9a6..0afcf70 100644
--- a/gopls/test/gopls_test.go
+++ b/gopls/test/gopls_test.go
@@ -10,7 +10,11 @@
"golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/gopls/internal/hooks"
+ "golang.org/x/tools/internal/jsonrpc2/servertest"
+ "golang.org/x/tools/internal/lsp/cache"
cmdtest "golang.org/x/tools/internal/lsp/cmd/test"
+ "golang.org/x/tools/internal/lsp/debug"
+ "golang.org/x/tools/internal/lsp/lsprpc"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/tests"
"golang.org/x/tools/internal/testenv"
@@ -37,6 +41,16 @@
t.Skip("testdata directory not present")
}
data := tests.Load(t, exporter, testdata)
- defer data.Exported.Cleanup()
- tests.Run(t, cmdtest.NewRunner(exporter, data, tests.Context(t), commandLineOptions), data)
+ ctx := tests.Context(t)
+ di := debug.NewInstance("", "")
+ cache := cache.New(commandLineOptions, di.State)
+ ss := lsprpc.NewStreamServer(cache, false, di)
+ ts := servertest.NewTCPServer(ctx, ss)
+ for _, data := range data {
+ defer data.Exported.Cleanup()
+ t.Run(data.Folder, func(t *testing.T) {
+ t.Helper()
+ tests.Run(t, cmdtest.NewRunner(exporter, data, tests.Context(t), ts.Addr, commandLineOptions), data)
+ })
+ }
}
diff --git a/internal/fastwalk/fastwalk.go b/internal/fastwalk/fastwalk.go
index 7219c8e..9887f7e 100644
--- a/internal/fastwalk/fastwalk.go
+++ b/internal/fastwalk/fastwalk.go
@@ -14,14 +14,14 @@
"sync"
)
-// TraverseLink is used as a return value from WalkFuncs to indicate that the
+// ErrTraverseLink is used as a return value from WalkFuncs to indicate that the
// symlink named in the call may be traversed.
-var TraverseLink = errors.New("fastwalk: traverse symlink, assuming target is a directory")
+var ErrTraverseLink = errors.New("fastwalk: traverse symlink, assuming target is a directory")
-// SkipFiles is a used as a return value from WalkFuncs to indicate that the
+// ErrSkipFiles is a used as a return value from WalkFuncs to indicate that the
// callback should not be called for any other files in the current directory.
// Child directories will still be traversed.
-var SkipFiles = errors.New("fastwalk: skip remaining files in directory")
+var ErrSkipFiles = errors.New("fastwalk: skip remaining files in directory")
// Walk is a faster implementation of filepath.Walk.
//
@@ -167,7 +167,7 @@
err := w.fn(joined, typ)
if typ == os.ModeSymlink {
- if err == TraverseLink {
+ if err == ErrTraverseLink {
// Set callbackDone so we don't call it twice for both the
// symlink-as-symlink and the symlink-as-directory later:
w.enqueue(walkItem{dir: joined, callbackDone: true})
diff --git a/internal/fastwalk/fastwalk_portable.go b/internal/fastwalk/fastwalk_portable.go
index a906b87..b0d6327 100644
--- a/internal/fastwalk/fastwalk_portable.go
+++ b/internal/fastwalk/fastwalk_portable.go
@@ -26,7 +26,7 @@
continue
}
if err := fn(dirName, fi.Name(), fi.Mode()&os.ModeType); err != nil {
- if err == SkipFiles {
+ if err == ErrSkipFiles {
skipFiles = true
continue
}
diff --git a/internal/fastwalk/fastwalk_test.go b/internal/fastwalk/fastwalk_test.go
index a122ddf..a6d9bea 100644
--- a/internal/fastwalk/fastwalk_test.go
+++ b/internal/fastwalk/fastwalk_test.go
@@ -184,7 +184,7 @@
mu.Lock()
defer mu.Unlock()
want["/src/"+filepath.Base(path)] = 0
- return fastwalk.SkipFiles
+ return fastwalk.ErrSkipFiles
}
return nil
},
@@ -208,7 +208,7 @@
},
func(path string, typ os.FileMode) error {
if typ == os.ModeSymlink {
- return fastwalk.TraverseLink
+ return fastwalk.ErrTraverseLink
}
return nil
},
diff --git a/internal/fastwalk/fastwalk_unix.go b/internal/fastwalk/fastwalk_unix.go
index 3369b1a..ce38fdc 100644
--- a/internal/fastwalk/fastwalk_unix.go
+++ b/internal/fastwalk/fastwalk_unix.go
@@ -66,7 +66,7 @@
continue
}
if err := fn(dirName, name, typ); err != nil {
- if err == SkipFiles {
+ if err == ErrSkipFiles {
skipFiles = true
continue
}
diff --git a/internal/gocommand/invoke.go b/internal/gocommand/invoke.go
new file mode 100644
index 0000000..75d73e7
--- /dev/null
+++ b/internal/gocommand/invoke.go
@@ -0,0 +1,121 @@
+// Package gocommand is a helper for calling the go command.
+package gocommand
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+)
+
+// An Invocation represents a call to the go command.
+type Invocation struct {
+ Verb string
+ Args []string
+ BuildFlags []string
+ Env []string
+ WorkingDir string
+ Logf func(format string, args ...interface{})
+}
+
+// Run runs the invocation, returning its stdout and an error suitable for
+// human consumption, including stderr.
+func (i *Invocation) Run(ctx context.Context) (*bytes.Buffer, error) {
+ stdout, _, friendly, _ := i.RunRaw(ctx)
+ return stdout, friendly
+}
+
+// RunRaw is like Run, but also returns the raw stderr and error for callers
+// that want to do low-level error handling/recovery.
+func (i *Invocation) RunRaw(ctx context.Context) (stdout *bytes.Buffer, stderr *bytes.Buffer, friendlyError error, rawError error) {
+ log := i.Logf
+ if log == nil {
+ log = func(string, ...interface{}) {}
+ }
+
+ goArgs := []string{i.Verb}
+ switch i.Verb {
+ case "mod":
+ // mod needs the sub-verb before build flags.
+ goArgs = append(goArgs, i.Args[0])
+ goArgs = append(goArgs, i.BuildFlags...)
+ goArgs = append(goArgs, i.Args[1:]...)
+ case "env":
+ // env doesn't take build flags.
+ goArgs = append(goArgs, i.Args...)
+ default:
+ goArgs = append(goArgs, i.BuildFlags...)
+ goArgs = append(goArgs, i.Args...)
+ }
+ cmd := exec.Command("go", goArgs...)
+ stdout = &bytes.Buffer{}
+ stderr = &bytes.Buffer{}
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+ // On darwin the cwd gets resolved to the real path, which breaks anything that
+ // expects the working directory to keep the original path, including the
+ // go command when dealing with modules.
+ // The Go stdlib has a special feature where if the cwd and the PWD are the
+ // same node then it trusts the PWD, so by setting it in the env for the child
+ // process we fix up all the paths returned by the go command.
+ cmd.Env = append(append([]string{}, i.Env...), "PWD="+i.WorkingDir)
+ cmd.Dir = i.WorkingDir
+
+ defer func(start time.Time) { log("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
+
+ rawError = runCmdContext(ctx, cmd)
+ friendlyError = rawError
+ if rawError != nil {
+ // Check for 'go' executable not being found.
+ if ee, ok := rawError.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
+ friendlyError = fmt.Errorf("go command required, not found: %v", ee)
+ }
+ if ctx.Err() != nil {
+ friendlyError = ctx.Err()
+ }
+ friendlyError = fmt.Errorf("err: %v: stderr: %s", rawError, stderr)
+ }
+ return
+}
+
+// runCmdContext is like exec.CommandContext except it sends os.Interrupt
+// before os.Kill.
+func runCmdContext(ctx context.Context, cmd *exec.Cmd) error {
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ resChan := make(chan error, 1)
+ go func() {
+ resChan <- cmd.Wait()
+ }()
+
+ select {
+ case err := <-resChan:
+ return err
+ case <-ctx.Done():
+ }
+ // Cancelled. Interrupt and see if it ends voluntarily.
+ cmd.Process.Signal(os.Interrupt)
+ select {
+ case err := <-resChan:
+ return err
+ case <-time.After(time.Second):
+ }
+ // Didn't shut down in response to interrupt. Kill it hard.
+ cmd.Process.Kill()
+ return <-resChan
+}
+
+func cmdDebugStr(cmd *exec.Cmd) string {
+ env := make(map[string]string)
+ for _, kv := range cmd.Env {
+ split := strings.Split(kv, "=")
+ k, v := split[0], split[1]
+ env[k] = v
+ }
+
+ return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v GOPROXY=%v PWD=%v go %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["GOPROXY"], env["PWD"], cmd.Args)
+}
diff --git a/internal/gopathwalk/walk.go b/internal/gopathwalk/walk.go
index d067562..64309db 100644
--- a/internal/gopathwalk/walk.go
+++ b/internal/gopathwalk/walk.go
@@ -189,14 +189,14 @@
if dir == w.root.Path && (w.root.Type == RootGOROOT || w.root.Type == RootGOPATH) {
// Doesn't make sense to have regular files
// directly in your $GOPATH/src or $GOROOT/src.
- return fastwalk.SkipFiles
+ return fastwalk.ErrSkipFiles
}
if !strings.HasSuffix(path, ".go") {
return nil
}
w.add(w.root, dir)
- return fastwalk.SkipFiles
+ return fastwalk.ErrSkipFiles
}
if typ == os.ModeDir {
base := filepath.Base(path)
@@ -224,7 +224,7 @@
return nil
}
if w.shouldTraverse(dir, fi) {
- return fastwalk.TraverseLink
+ return fastwalk.ErrTraverseLink
}
}
return nil
diff --git a/internal/imports/fix.go b/internal/imports/fix.go
index e94d47e..5e0c9df 100644
--- a/internal/imports/fix.go
+++ b/internal/imports/fix.go
@@ -14,7 +14,6 @@
"go/token"
"io/ioutil"
"os"
- "os/exec"
"path"
"path/filepath"
"reflect"
@@ -22,11 +21,11 @@
"strconv"
"strings"
"sync"
- "time"
"unicode"
"unicode/utf8"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/gopathwalk"
)
@@ -537,7 +536,7 @@
// derive package names from import paths, see if the file is already
// complete. We can't add any imports yet, because we don't know
// if missing references are actually package vars.
- p := &pass{fset: fset, f: f, srcDir: srcDir}
+ p := &pass{fset: fset, f: f, srcDir: srcDir, env: env}
if fixes, done := p.load(); done {
return fixes, nil
}
@@ -559,8 +558,7 @@
}
// Third pass: get real package names where we had previously used
- // the naive algorithm. This is the first step that will use the
- // environment, so we provide it here for the first time.
+ // the naive algorithm.
p = &pass{fset: fset, f: f, srcDir: srcDir, env: env}
p.loadRealPackageNames = true
p.otherFiles = otherFiles
@@ -793,7 +791,7 @@
if e.resolver != nil {
return e.resolver
}
- out, err := e.invokeGo("env", "GOMOD")
+ out, err := e.invokeGo(context.TODO(), "env", "GOMOD")
if err != nil || len(bytes.TrimSpace(out.Bytes())) == 0 {
e.resolver = newGopathResolver(e)
return e.resolver
@@ -824,42 +822,24 @@
return &ctx
}
-func (e *ProcessEnv) invokeGo(verb string, args ...string) (*bytes.Buffer, error) {
- goArgs := []string{verb}
- if verb != "env" {
- goArgs = append(goArgs, e.BuildFlags...)
+func (e *ProcessEnv) invokeGo(ctx context.Context, verb string, args ...string) (*bytes.Buffer, error) {
+ inv := gocommand.Invocation{
+ Verb: verb,
+ Args: args,
+ BuildFlags: e.BuildFlags,
+ Env: e.env(),
+ Logf: e.Logf,
+ WorkingDir: e.WorkingDir,
}
- goArgs = append(goArgs, args...)
- cmd := exec.Command("go", goArgs...)
- stdout := &bytes.Buffer{}
- stderr := &bytes.Buffer{}
- cmd.Stdout = stdout
- cmd.Stderr = stderr
- cmd.Env = e.env()
- cmd.Dir = e.WorkingDir
-
- if e.Debug {
- defer func(start time.Time) { e.Logf("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
- }
- if err := cmd.Run(); err != nil {
- return nil, fmt.Errorf("running go: %v (stderr:\n%s)", err, stderr)
- }
- return stdout, nil
-}
-
-func cmdDebugStr(cmd *exec.Cmd) string {
- env := make(map[string]string)
- for _, kv := range cmd.Env {
- split := strings.Split(kv, "=")
- k, v := split[0], split[1]
- env[k] = v
- }
-
- return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v GOPROXY=%v PWD=%v go %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["GOPROXY"], env["PWD"], cmd.Args)
+ return inv.Run(ctx)
}
func addStdlibCandidates(pass *pass, refs references) {
add := func(pkg string) {
+ // Prevent self-imports.
+ if path.Base(pkg) == pass.f.Name.Name && filepath.Join(pass.env.GOROOT, "src", pkg) == pass.srcDir {
+ return
+ }
exports := copyExports(stdlib[pkg])
pass.addCandidate(
&ImportInfo{ImportPath: pkg},
diff --git a/internal/imports/fix_test.go b/internal/imports/fix_test.go
index 67ebe53..f2bc5b6 100644
--- a/internal/imports/fix_test.go
+++ b/internal/imports/fix_test.go
@@ -1577,6 +1577,27 @@
})
}
+func TestStdlibSelfImports(t *testing.T) {
+ const input = `package ecdsa
+
+var _ = ecdsa.GenerateKey
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "ignored.com",
+ },
+ }.test(t, func(t *goimportTest) {
+ got, err := t.processNonModule(filepath.Join(t.env.GOROOT, "src/crypto/ecdsa/foo.go"), []byte(input), nil)
+ if err != nil {
+ t.Fatalf("Process() = %v", err)
+ }
+ if string(got) != input {
+ t.Errorf("Got:\n%s\nWant:\n%s", got, input)
+ }
+ })
+}
+
type testConfig struct {
gopathOnly bool
module packagestest.Module
@@ -2665,7 +2686,7 @@
defer wg.Done()
_, err := t.process("foo.com", "p/first.go", nil, nil)
if err != nil {
- t.Fatal(err)
+ t.Error(err)
}
}()
}
diff --git a/internal/imports/mod.go b/internal/imports/mod.go
index 3ae859e..28d4b1f 100644
--- a/internal/imports/mod.go
+++ b/internal/imports/mod.go
@@ -14,9 +14,9 @@
"strconv"
"strings"
+ "golang.org/x/mod/module"
+ "golang.org/x/mod/semver"
"golang.org/x/tools/internal/gopathwalk"
- "golang.org/x/tools/internal/module"
- "golang.org/x/tools/internal/semver"
)
// ModuleResolver implements resolver for modules using the go command as little
@@ -146,7 +146,7 @@
}
func (r *ModuleResolver) initAllMods() error {
- stdout, err := r.env.invokeGo("list", "-m", "-json", "...")
+ stdout, err := r.env.invokeGo(context.TODO(), "list", "-m", "-json", "...")
if err != nil {
return err
}
@@ -162,6 +162,8 @@
// Can't do anything with a module that's not downloaded.
continue
}
+ // golang/go#36193: the go command doesn't always clean paths.
+ mod.Dir = filepath.Clean(mod.Dir)
r.modsByModPath = append(r.modsByModPath, mod)
r.modsByDir = append(r.modsByDir, mod)
if mod.Main {
@@ -579,7 +581,7 @@
err: fmt.Errorf("invalid module cache path: %v", subdir),
}
}
- modPath, err := module.DecodePath(filepath.ToSlash(matches[1]))
+ modPath, err := module.UnescapePath(filepath.ToSlash(matches[1]))
if err != nil {
if r.env.Debug {
r.env.Logf("decoding module cache path %q: %v", subdir, err)
@@ -697,7 +699,7 @@
{{.GoVersion}}
{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}
`
- stdout, err := env.invokeGo("list", "-m", "-f", format)
+ stdout, err := env.invokeGo(context.TODO(), "list", "-m", "-f", format)
if err != nil {
return nil, false, nil
}
diff --git a/internal/imports/mod_112_test.go b/internal/imports/mod_112_test.go
index 0305f48..106db7c 100644
--- a/internal/imports/mod_112_test.go
+++ b/internal/imports/mod_112_test.go
@@ -3,6 +3,7 @@
package imports
import (
+ "context"
"testing"
)
@@ -13,7 +14,7 @@
package x
`, "")
defer mt.cleanup()
- if _, err := mt.env.invokeGo("mod", "download", "rsc.io/quote@v1.5.1"); err != nil {
+ if _, err := mt.env.invokeGo(context.Background(), "mod", "download", "rsc.io/quote@v1.5.1"); err != nil {
t.Fatal(err)
}
diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go
index 882be4b..8df024f 100644
--- a/internal/imports/mod_test.go
+++ b/internal/imports/mod_test.go
@@ -18,8 +18,8 @@
"sync"
"testing"
+ "golang.org/x/mod/module"
"golang.org/x/tools/internal/gopathwalk"
- "golang.org/x/tools/internal/module"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/txtar"
)
@@ -231,10 +231,10 @@
mt.assertModuleFoundInDir("rsc.io/sampler", "sampler", `pkg.*mod.*/sampler@.*$`)
// Populate vendor/ and clear out the mod cache so we can't cheat.
- if _, err := mt.env.invokeGo("mod", "vendor"); err != nil {
+ if _, err := mt.env.invokeGo(context.Background(), "mod", "vendor"); err != nil {
t.Fatal(err)
}
- if _, err := mt.env.invokeGo("clean", "-modcache"); err != nil {
+ if _, err := mt.env.invokeGo(context.Background(), "clean", "-modcache"); err != nil {
t.Fatal(err)
}
@@ -259,7 +259,7 @@
defer mt.cleanup()
// Populate vendor/.
- if _, err := mt.env.invokeGo("mod", "vendor"); err != nil {
+ if _, err := mt.env.invokeGo(context.Background(), "mod", "vendor"); err != nil {
t.Fatal(err)
}
@@ -686,7 +686,7 @@
t.Fatalf("checking if go.mod exists: %v", err)
}
if err == nil {
- if _, err := env.invokeGo("mod", "download"); err != nil {
+ if _, err := env.invokeGo(context.Background(), "mod", "download"); err != nil {
t.Fatal(err)
}
}
@@ -739,7 +739,7 @@
i := strings.LastIndex(arName, "_v")
ver := strings.TrimSuffix(arName[i+1:], ".txt")
modDir := strings.Replace(arName[:i], "_", "/", -1)
- modPath, err := module.DecodePath(modDir)
+ modPath, err := module.UnescapePath(modDir)
if err != nil {
return err
}
@@ -868,7 +868,7 @@
`, "")
defer mt.cleanup()
- if _, err := mt.env.invokeGo("mod", "download", "rsc.io/quote/v2@v2.0.1"); err != nil {
+ if _, err := mt.env.invokeGo(context.Background(), "mod", "download", "rsc.io/quote/v2@v2.0.1"); err != nil {
t.Fatal(err)
}
diff --git a/internal/jsonrpc2/jsonrpc2.go b/internal/jsonrpc2/jsonrpc2.go
index 963f818..39ac3eb 100644
--- a/internal/jsonrpc2/jsonrpc2.go
+++ b/internal/jsonrpc2/jsonrpc2.go
@@ -147,14 +147,15 @@
for _, h := range c.handlers {
ctx = h.Request(ctx, c, Send, request)
}
- // we have to add ourselves to the pending map before we send, otherwise we
- // are racing the response
- rchan := make(chan *WireResponse)
+ // We have to add ourselves to the pending map before we send, otherwise we
+ // are racing the response. Also add a buffer to rchan, so that if we get a
+ // wire response between the time this call is cancelled and id is deleted
+ // from c.pending, the send to rchan will not block.
+ rchan := make(chan *WireResponse, 1)
c.pendingMu.Lock()
c.pending[id] = rchan
c.pendingMu.Unlock()
defer func() {
- // clean up the pending response handler on the way out
c.pendingMu.Lock()
delete(c.pending, id)
c.pendingMu.Unlock()
@@ -189,7 +190,7 @@
}
return nil
case <-ctx.Done():
- // allow the handler to propagate the cancel
+ // Allow the handler to propagate the cancel.
cancelled := false
for _, h := range c.handlers {
if h.Cancel(ctx, c, id, cancelled) {
@@ -315,7 +316,8 @@
// get the data for a message
data, n, err := c.stream.Read(runCtx)
if err != nil {
- // the stream failed, we cannot continue
+ // The stream failed, we cannot continue. If the client disconnected
+ // normally, we should get ErrDisconnected here.
return err
}
// read a combined message
@@ -328,10 +330,10 @@
}
continue
}
- // work out which kind of message we have
+ // Work out whether this is a request or response.
switch {
case msg.Method != "":
- // if method is set it must be a request
+ // If method is set it must be a request.
reqCtx, cancelReq := context.WithCancel(runCtx)
thisRequest := nextRequest
nextRequest = make(chan struct{})
@@ -373,21 +375,19 @@
}
}()
case msg.ID != nil:
- // we have a response, get the pending entry from the map
+ // If method is not set, this should be a response, in which case we must
+ // have an id to send the response back to the caller.
c.pendingMu.Lock()
- rchan := c.pending[*msg.ID]
- if rchan != nil {
- delete(c.pending, *msg.ID)
- }
+ rchan, ok := c.pending[*msg.ID]
c.pendingMu.Unlock()
- // and send the reply to the channel
- response := &WireResponse{
- Result: msg.Result,
- Error: msg.Error,
- ID: msg.ID,
+ if ok {
+ response := &WireResponse{
+ Result: msg.Result,
+ Error: msg.Error,
+ ID: msg.ID,
+ }
+ rchan <- response
}
- rchan <- response
- close(rchan)
default:
for _, h := range c.handlers {
h.Error(runCtx, fmt.Errorf("message not a call, notify or response, ignoring"))
diff --git a/internal/jsonrpc2/jsonrpc2_test.go b/internal/jsonrpc2/jsonrpc2_test.go
index 192a5e8..4fcca31 100644
--- a/internal/jsonrpc2/jsonrpc2_test.go
+++ b/internal/jsonrpc2/jsonrpc2_test.go
@@ -115,7 +115,7 @@
w.Close()
}()
if err := conn.Run(ctx); err != nil {
- t.Fatalf("Stream failed: %v", err)
+ t.Errorf("Stream failed: %v", err)
}
}()
return conn
diff --git a/internal/jsonrpc2/serve.go b/internal/jsonrpc2/serve.go
new file mode 100644
index 0000000..c2cfa60
--- /dev/null
+++ b/internal/jsonrpc2/serve.go
@@ -0,0 +1,115 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package jsonrpc2
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log"
+ "net"
+ "os"
+ "time"
+)
+
+// NOTE: This file provides an experimental API for serving multiple remote
+// jsonrpc2 clients over the network. For now, it is intentionally similar to
+// net/http, but that may change in the future as we figure out the correct
+// semantics.
+
+// A StreamServer is used to serve incoming jsonrpc2 clients communicating over
+// a newly created stream.
+type StreamServer interface {
+ ServeStream(context.Context, Stream) error
+}
+
+// The ServerFunc type is an adapter that implements the StreamServer interface
+// using an ordinary function.
+type ServerFunc func(context.Context, Stream) error
+
+// ServeStream calls f(ctx, s).
+func (f ServerFunc) ServeStream(ctx context.Context, s Stream) error {
+ return f(ctx, s)
+}
+
+// HandlerServer returns a StreamServer that handles incoming streams using the
+// provided handler.
+func HandlerServer(h Handler) StreamServer {
+ return ServerFunc(func(ctx context.Context, s Stream) error {
+ conn := NewConn(s)
+ conn.AddHandler(h)
+ return conn.Run(ctx)
+ })
+}
+
+// ListenAndServe starts an jsonrpc2 server on the given address. If
+// idleTimeout is non-zero, ListenAndServe exits after there are no clients for
+// this duration, otherwise it exits only on error.
+func ListenAndServe(ctx context.Context, network, addr string, server StreamServer, idleTimeout time.Duration) error {
+ ln, err := net.Listen(network, addr)
+ if err != nil {
+ return err
+ }
+ defer ln.Close()
+ if network == "unix" {
+ defer os.Remove(addr)
+ }
+ return Serve(ctx, ln, server, idleTimeout)
+}
+
+// ErrIdleTimeout is returned when serving timed out waiting for new connections.
+var ErrIdleTimeout = errors.New("timed out waiting for new connections")
+
+// Serve accepts incoming connections from the network, and handles them using
+// the provided server. If idleTimeout is non-zero, ListenAndServe exits after
+// there are no clients for this duration, otherwise it exits only on error.
+func Serve(ctx context.Context, ln net.Listener, server StreamServer, idleTimeout time.Duration) error {
+ // Max duration: ~290 years; surely that's long enough.
+ const forever = 1<<63 - 1
+ if idleTimeout <= 0 {
+ idleTimeout = forever
+ }
+ connTimer := time.NewTimer(idleTimeout)
+
+ newConns := make(chan net.Conn)
+ doneListening := make(chan error)
+ closedConns := make(chan error)
+
+ go func() {
+ for {
+ nc, err := ln.Accept()
+ if err != nil {
+ doneListening <- fmt.Errorf("Accept(): %v", err)
+ return
+ }
+ newConns <- nc
+ }
+ }()
+
+ activeConns := 0
+ for {
+ select {
+ case netConn := <-newConns:
+ activeConns++
+ connTimer.Stop()
+ stream := NewHeaderStream(netConn, netConn)
+ go func() {
+ closedConns <- server.ServeStream(ctx, stream)
+ }()
+ case err := <-doneListening:
+ return err
+ case err := <-closedConns:
+ log.Printf("closed a connection with error: %v", err)
+ activeConns--
+ if activeConns == 0 {
+ connTimer.Reset(idleTimeout)
+ }
+ case <-connTimer.C:
+ return ErrIdleTimeout
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
diff --git a/internal/jsonrpc2/serve_test.go b/internal/jsonrpc2/serve_test.go
new file mode 100644
index 0000000..ec6e1e1
--- /dev/null
+++ b/internal/jsonrpc2/serve_test.go
@@ -0,0 +1,59 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package jsonrpc2
+
+import (
+ "context"
+ "net"
+ "sync"
+ "testing"
+ "time"
+)
+
+func TestIdleTimeout(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ ln, err := net.Listen("tcp", ":0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ln.Close()
+
+ connect := func() net.Conn {
+ conn, err := net.DialTimeout("tcp", ln.Addr().String(), 5*time.Second)
+ if err != nil {
+ panic(err)
+ }
+ return conn
+ }
+
+ server := HandlerServer(EmptyHandler{})
+ // connTimer := &fakeTimer{c: make(chan time.Time, 1)}
+ var (
+ runErr error
+ wg sync.WaitGroup
+ )
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ runErr = Serve(ctx, ln, server, 100*time.Millisecond)
+ }()
+
+ // Exercise some connection/disconnection patterns, and then assert that when
+ // our timer fires, the server exits.
+ conn1 := connect()
+ conn2 := connect()
+ conn1.Close()
+ conn2.Close()
+ conn3 := connect()
+ conn3.Close()
+
+ wg.Wait()
+
+ if runErr != ErrIdleTimeout {
+ t.Errorf("run() returned error %v, want %v", runErr, ErrIdleTimeout)
+ }
+}
diff --git a/internal/jsonrpc2/servertest/servertest.go b/internal/jsonrpc2/servertest/servertest.go
new file mode 100644
index 0000000..1217591
--- /dev/null
+++ b/internal/jsonrpc2/servertest/servertest.go
@@ -0,0 +1,126 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package servertest provides utilities for running tests against a remote LSP
+// server.
+package servertest
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net"
+ "sync"
+
+ "golang.org/x/tools/internal/jsonrpc2"
+)
+
+// Connector is the interface used to connect to a server.
+type Connector interface {
+ Connect(context.Context) *jsonrpc2.Conn
+}
+
+// TCPServer is a helper for executing tests against a remote jsonrpc2
+// connection. Once initialized, its Addr field may be used to connect a
+// jsonrpc2 client.
+type TCPServer struct {
+ Addr string
+
+ ln net.Listener
+ cls *closerList
+}
+
+// NewTCPServer returns a new test server listening on local tcp port and
+// serving incoming jsonrpc2 streams using the provided stream server. It
+// panics on any error.
+func NewTCPServer(ctx context.Context, server jsonrpc2.StreamServer) *TCPServer {
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ panic(fmt.Sprintf("servertest: failed to listen: %v", err))
+ }
+ go jsonrpc2.Serve(ctx, ln, server, 0)
+ return &TCPServer{Addr: ln.Addr().String(), ln: ln, cls: &closerList{}}
+}
+
+// Connect dials the test server and returns a jsonrpc2 Connection that is
+// ready for use.
+func (s *TCPServer) Connect(ctx context.Context) *jsonrpc2.Conn {
+ netConn, err := net.Dial("tcp", s.Addr)
+ if err != nil {
+ panic(fmt.Sprintf("servertest: failed to connect to test instance: %v", err))
+ }
+ s.cls.add(func() {
+ netConn.Close()
+ })
+ conn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn, netConn))
+ go conn.Run(ctx)
+ return conn
+}
+
+// Close closes all connected pipes.
+func (s *TCPServer) Close() error {
+ s.cls.closeAll()
+ return nil
+}
+
+// PipeServer is a test server that handles connections over io.Pipes.
+type PipeServer struct {
+ server jsonrpc2.StreamServer
+ cls *closerList
+}
+
+// NewPipeServer returns a test server that can be connected to via io.Pipes.
+func NewPipeServer(ctx context.Context, server jsonrpc2.StreamServer) *PipeServer {
+ return &PipeServer{server: server, cls: &closerList{}}
+}
+
+// Connect creates new io.Pipes and binds them to the underlying StreamServer.
+func (s *PipeServer) Connect(ctx context.Context) *jsonrpc2.Conn {
+ // Pipes connect like this:
+ // Client🡒(sWriter)🡒(sReader)🡒Server
+ // 🡔(cReader)🡐(cWriter)🡗
+ sReader, sWriter := io.Pipe()
+ cReader, cWriter := io.Pipe()
+ s.cls.add(func() {
+ sReader.Close()
+ sWriter.Close()
+ cReader.Close()
+ cWriter.Close()
+ })
+ serverStream := jsonrpc2.NewStream(sReader, cWriter)
+ go s.server.ServeStream(ctx, serverStream)
+
+ clientStream := jsonrpc2.NewStream(cReader, sWriter)
+ clientConn := jsonrpc2.NewConn(clientStream)
+ go clientConn.Run(ctx)
+ return clientConn
+}
+
+// Close closes all connected pipes.
+func (s *PipeServer) Close() error {
+ s.cls.closeAll()
+ return nil
+}
+
+// closerList tracks closers to run when a testserver is closed. This is a
+// convenience, so that callers don't have to worry about closing each
+// connection.
+type closerList struct {
+ mu sync.Mutex
+ closers []func()
+}
+
+func (l *closerList) add(closer func()) {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.closers = append(l.closers, closer)
+}
+
+func (l *closerList) closeAll() {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ for _, closer := range l.closers {
+ closer()
+ }
+}
diff --git a/internal/jsonrpc2/servertest/servertest_test.go b/internal/jsonrpc2/servertest/servertest_test.go
new file mode 100644
index 0000000..21b540a
--- /dev/null
+++ b/internal/jsonrpc2/servertest/servertest_test.go
@@ -0,0 +1,59 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package servertest
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "golang.org/x/tools/internal/jsonrpc2"
+)
+
+type fakeHandler struct {
+ jsonrpc2.EmptyHandler
+}
+
+type msg struct {
+ Msg string
+}
+
+func (fakeHandler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
+ if err := r.Reply(ctx, &msg{"pong"}, nil); err != nil {
+ panic(err)
+ }
+ return true
+}
+
+func TestTestServer(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ server := jsonrpc2.HandlerServer(fakeHandler{})
+ tcpTS := NewTCPServer(ctx, server)
+ defer tcpTS.Close()
+ pipeTS := NewPipeServer(ctx, server)
+ defer pipeTS.Close()
+
+ tests := []struct {
+ name string
+ connector Connector
+ }{
+ {"tcp", tcpTS},
+ {"pipe", pipeTS},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ conn := test.connector.Connect(ctx)
+ var got msg
+ if err := conn.Call(ctx, "ping", &msg{"ping"}, &got); err != nil {
+ t.Fatal(err)
+ }
+ if want := "pong"; got.Msg != want {
+ t.Errorf("conn.Call(...): returned %q, want %q", got, want)
+ }
+ })
+ }
+}
diff --git a/internal/jsonrpc2/stream.go b/internal/jsonrpc2/stream.go
index f850c27..2d1e7c4 100644
--- a/internal/jsonrpc2/stream.go
+++ b/internal/jsonrpc2/stream.go
@@ -8,6 +8,7 @@
"bufio"
"context"
"encoding/json"
+ "errors"
"fmt"
"io"
"strconv"
@@ -28,6 +29,9 @@
Write(context.Context, []byte) (int64, error)
}
+// ErrDisconnected signals that the stream or connection exited normally.
+var ErrDisconnected = errors.New("disconnected")
+
// NewStream returns a Stream built on top of an io.Reader and io.Writer
// The messages are sent with no wrapping, and rely on json decode consistency
// to determine message boundaries.
@@ -52,6 +56,9 @@
}
var raw json.RawMessage
if err := s.in.Decode(&raw); err != nil {
+ if err == io.EOF {
+ return nil, 0, ErrDisconnected
+ }
return nil, 0, err
}
return raw, int64(len(raw)), nil
@@ -96,6 +103,10 @@
for {
line, err := s.in.ReadString('\n')
total += int64(len(line))
+ if err == io.EOF {
+ // A normal disconnection will terminate with EOF before the next header.
+ return nil, total, ErrDisconnected
+ }
if err != nil {
return nil, total, fmt.Errorf("failed reading header line %q", err)
}
diff --git a/internal/jsonrpc2/wire.go b/internal/jsonrpc2/wire.go
index 3e891e0..127feb1 100644
--- a/internal/jsonrpc2/wire.go
+++ b/internal/jsonrpc2/wire.go
@@ -104,7 +104,7 @@
return err
}
if version != "2.0" {
- return fmt.Errorf("Invalid RPC version %v", version)
+ return fmt.Errorf("invalid RPC version %v", version)
}
return nil
}
diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go
index f4c99cd..2fd4714 100644
--- a/internal/lsp/cache/analysis.go
+++ b/internal/lsp/cache/analysis.go
@@ -27,7 +27,7 @@
var roots []*actionHandle
for _, a := range analyzers {
- ah, err := s.actionHandle(ctx, packageID(id), source.ParseFull, a)
+ ah, err := s.actionHandle(ctx, packageID(id), a)
if err != nil {
return nil, err
}
@@ -50,6 +50,8 @@
return results, nil
}
+type actionHandleKey string
+
// An action represents one unit of analysis work: the application of
// one analysis to one package. Actions form a DAG, both within a
// package (as different analyzers are applied, either in sequence or
@@ -79,15 +81,15 @@
typ reflect.Type
}
-func (s *snapshot) actionHandle(ctx context.Context, id packageID, mode source.ParseMode, a *analysis.Analyzer) (*actionHandle, error) {
- act := s.getActionHandle(id, mode, a)
+func (s *snapshot) actionHandle(ctx context.Context, id packageID, a *analysis.Analyzer) (*actionHandle, error) {
+ ph := s.getPackage(id, source.ParseFull)
+ if ph == nil {
+ return nil, errors.Errorf("no PackageHandle for %s", id)
+ }
+ act := s.getActionHandle(id, ph.mode, a)
if act != nil {
return act, nil
}
- ph := s.getPackage(id, mode)
- if ph == nil {
- return nil, errors.Errorf("no PackageHandle for %s:%v", id, mode == source.ParseExported)
- }
if len(ph.key) == 0 {
return nil, errors.Errorf("no key for PackageHandle %s", id)
}
@@ -102,7 +104,7 @@
var deps []*actionHandle
// Add a dependency on each required analyzers.
for _, req := range a.Requires {
- reqActionHandle, err := s.actionHandle(ctx, id, mode, req)
+ reqActionHandle, err := s.actionHandle(ctx, id, req)
if err != nil {
return nil, err
}
@@ -122,7 +124,7 @@
}
sort.Strings(importIDs) // for determinism
for _, importID := range importIDs {
- depActionHandle, err := s.actionHandle(ctx, packageID(importID), source.ParseExported, a)
+ depActionHandle, err := s.actionHandle(ctx, packageID(importID), a)
if err != nil {
return nil, err
}
@@ -164,8 +166,8 @@
return data.diagnostics, data.result, data.err
}
-func buildActionKey(a *analysis.Analyzer, ph *packageHandle) string {
- return hashContents([]byte(fmt.Sprintf("%p %s", a, string(ph.key))))
+func buildActionKey(a *analysis.Analyzer, ph *packageHandle) actionHandleKey {
+ return actionHandleKey(hashContents([]byte(fmt.Sprintf("%p %s", a, string(ph.key)))))
}
func (act *actionHandle) String() string {
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index 56531dd..4b71ebc 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -9,6 +9,7 @@
"crypto/sha1"
"fmt"
"go/token"
+ "reflect"
"strconv"
"sync/atomic"
@@ -18,23 +19,28 @@
"golang.org/x/tools/internal/span"
)
-func New(options func(*source.Options)) source.Cache {
+func New(options func(*source.Options), debugState *debug.State) *Cache {
+ if debugState == nil {
+ debugState = &debug.State{}
+ }
index := atomic.AddInt64(&cacheIndex, 1)
- c := &cache{
+ c := &Cache{
fs: &nativeFileSystem{},
id: strconv.FormatInt(index, 10),
fset: token.NewFileSet(),
options: options,
+ debug: debugState,
}
- debug.AddCache(debugCache{c})
+ debugState.AddCache(debugCache{c})
return c
}
-type cache struct {
+type Cache struct {
fs source.FileSystem
id string
fset *token.FileSet
options func(*source.Options)
+ debug *debug.State
store memoize.Store
}
@@ -44,7 +50,7 @@
}
type fileHandle struct {
- cache *cache
+ cache *Cache
underlying source.FileHandle
handle *memoize.Handle
}
@@ -56,7 +62,7 @@
err error
}
-func (c *cache) GetFile(uri span.URI) source.FileHandle {
+func (c *Cache) GetFile(uri span.URI) source.FileHandle {
underlying := c.fs.GetFile(uri)
key := fileKey{
identity: underlying.Identity(),
@@ -73,19 +79,19 @@
}
}
-func (c *cache) NewSession() source.Session {
+func (c *Cache) NewSession() *Session {
index := atomic.AddInt64(&sessionIndex, 1)
- s := &session{
+ s := &Session{
cache: c,
id: strconv.FormatInt(index, 10),
- options: source.DefaultOptions,
+ options: source.DefaultOptions(),
overlays: make(map[span.URI]*overlay),
}
- debug.AddSession(debugSession{s})
+ c.debug.AddSession(DebugSession{s})
return s
}
-func (c *cache) FileSet() *token.FileSet {
+func (c *Cache) FileSet() *token.FileSet {
return c.fset
}
@@ -114,7 +120,8 @@
var cacheIndex, sessionIndex, viewIndex int64
-type debugCache struct{ *cache }
+type debugCache struct{ *Cache }
-func (c *cache) ID() string { return c.id }
-func (c debugCache) FileSet() *token.FileSet { return c.fset }
+func (c *Cache) ID() string { return c.id }
+func (c debugCache) FileSet() *token.FileSet { return c.fset }
+func (c debugCache) MemStats() map[reflect.Type]int { return c.store.Stats() }
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index b578bae..be472fd 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -13,6 +13,7 @@
"go/types"
"path"
"sort"
+ "strings"
"sync"
"golang.org/x/tools/go/packages"
@@ -25,6 +26,8 @@
errors "golang.org/x/xerrors"
)
+type packageHandleKey string
+
// packageHandle implements source.PackageHandle.
type packageHandle struct {
handle *memoize.Handle
@@ -41,7 +44,7 @@
m *metadata
// key is the hashed key for the package.
- key []byte
+ key packageHandleKey
}
func (ph *packageHandle) packageKey() packageKey {
@@ -61,7 +64,6 @@
// buildPackageHandle returns a source.PackageHandle for a given package and config.
func (s *snapshot) buildPackageHandle(ctx context.Context, id packageID, mode source.ParseMode) (*packageHandle, error) {
- // Check if we already have this PackageHandle cached.
if ph := s.getPackage(id, mode); ph != nil {
return ph, nil
}
@@ -86,7 +88,7 @@
key := ph.key
fset := s.view.session.cache.fset
- h := s.view.session.cache.store.Bind(string(key), func(ctx context.Context) interface{} {
+ h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
// Begin loading the direct dependencies, in parallel.
for _, dep := range deps {
go func(dep *packageHandle) {
@@ -134,7 +136,7 @@
deps := make(map[packagePath]*packageHandle)
// Begin computing the key by getting the depKeys for all dependencies.
- var depKeys [][]byte
+ var depKeys []packageHandleKey
for _, depID := range depList {
mode := source.ParseExported
if _, ok := s.isWorkspacePackage(depID); ok {
@@ -146,7 +148,7 @@
// One bad dependency should not prevent us from checking the entire package.
// Add a special key to mark a bad dependency.
- depKeys = append(depKeys, []byte(fmt.Sprintf("%s import not found", id)))
+ depKeys = append(depKeys, packageHandleKey(fmt.Sprintf("%s import not found", id)))
continue
}
deps[depHandle.m.pkgPath] = depHandle
@@ -156,8 +158,12 @@
return ph, deps, nil
}
-func checkPackageKey(id packageID, pghs []source.ParseGoHandle, cfg *packages.Config, deps [][]byte) []byte {
- return []byte(hashContents([]byte(fmt.Sprintf("%s%s%s%s", id, hashParseKeys(pghs), hashConfig(cfg), hashContents(bytes.Join(deps, nil))))))
+func checkPackageKey(id packageID, pghs []source.ParseGoHandle, cfg *packages.Config, deps []packageHandleKey) packageHandleKey {
+ var depBytes []byte
+ for _, dep := range deps {
+ depBytes = append(depBytes, []byte(dep)...)
+ }
+ return packageHandleKey(hashContents([]byte(fmt.Sprintf("%s%s%s%s", id, hashParseKeys(pghs), hashConfig(cfg), hashContents(depBytes)))))
}
// hashConfig returns the hash for the *packages.Config.
@@ -206,6 +212,28 @@
return md
}
+func hashImports(ctx context.Context, wsPackages []source.PackageHandle) (string, error) {
+ results := make(map[string]bool)
+ var imports []string
+ for _, ph := range wsPackages {
+ // Check package since we do not always invalidate the metadata.
+ pkg, err := ph.Check(ctx)
+ if err != nil {
+ return "", err
+ }
+ for _, path := range pkg.Imports() {
+ imp := path.PkgPath()
+ if _, ok := results[imp]; !ok {
+ results[imp] = true
+ imports = append(imports, imp)
+ }
+ }
+ }
+ sort.Strings(imports)
+ hashed := strings.Join(imports, ",")
+ return hashContents([]byte(hashed)), nil
+}
+
func (ph *packageHandle) Cached() (source.Package, error) {
return ph.cached()
}
@@ -246,6 +274,7 @@
mode: mode,
goFiles: goFiles,
compiledGoFiles: compiledGoFiles,
+ module: m.module,
imports: make(map[packagePath]*pkg),
typesSizes: m.typesSizes,
typesInfo: &types.Info{
@@ -256,6 +285,7 @@
Selections: make(map[*ast.SelectorExpr]*types.Selection),
Scopes: make(map[ast.Node]*types.Scope),
},
+ forTest: m.forTest,
}
var (
files = make([]*ast.File, len(pkg.compiledGoFiles))
@@ -266,7 +296,7 @@
for i, ph := range pkg.compiledGoFiles {
wg.Add(1)
go func(i int, ph source.ParseGoHandle) {
- files[i], _, parseErrors[i], actualErrors[i] = ph.Parse(ctx)
+ files[i], _, _, parseErrors[i], actualErrors[i] = ph.Parse(ctx)
wg.Done()
}(i, ph)
}
@@ -298,6 +328,9 @@
// Use the default type information for the unsafe package.
if pkg.pkgPath == "unsafe" {
pkg.types = types.Unsafe
+ // Don't type check Unsafe: it's unnecessary, and doing so exposes a data
+ // race to Unsafe.completed.
+ return pkg, nil
} else if len(files) == 0 { // not the unsafe package, no parsed files
return nil, errors.Errorf("no parsed files for package %s, expected: %s, errors: %v, list errors: %v", pkg.pkgPath, pkg.compiledGoFiles, actualErrors, rawErrors)
} else {
@@ -309,6 +342,10 @@
rawErrors = append(rawErrors, e)
},
Importer: importerFunc(func(pkgPath string) (*types.Package, error) {
+ // If the context was cancelled, we should abort.
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
dep := deps[packagePath(pkgPath)]
if dep == nil {
// We may be in GOPATH mode, in which case we need to check vendor dirs.
@@ -331,6 +368,9 @@
if dep == nil {
return nil, errors.Errorf("no package for import %s", pkgPath)
}
+ if !isValidImport(m.pkgPath, dep.m.pkgPath) {
+ return nil, errors.Errorf("invalid use of internal package %s", pkgPath)
+ }
depPkg, err := dep.check(ctx)
if err != nil {
return nil, err
@@ -363,6 +403,17 @@
return pkg, nil
}
+func isValidImport(pkgPath, importPkgPath packagePath) bool {
+ i := strings.LastIndex(string(importPkgPath), "/internal/")
+ if i == -1 {
+ return true
+ }
+ if pkgPath == "command-line-arguments" {
+ return true
+ }
+ return strings.HasPrefix(string(pkgPath), string(importPkgPath[:i]))
+}
+
// An importFunc is an implementation of the single-method
// types.Importer interface based on a function value.
type importerFunc func(path string) (*types.Package, error)
diff --git a/internal/lsp/cache/debug.go b/internal/lsp/cache/debug.go
index 95d3fa2..d4ea528 100644
--- a/internal/lsp/cache/debug.go
+++ b/internal/lsp/cache/debug.go
@@ -14,13 +14,14 @@
type debugView struct{ *view }
func (v debugView) ID() string { return v.id }
-func (v debugView) Session() debug.Session { return debugSession{v.session} }
+func (v debugView) Session() debug.Session { return DebugSession{v.session} }
+func (v debugView) Env() []string { return v.Options().Env }
-type debugSession struct{ *session }
+type DebugSession struct{ *Session }
-func (s debugSession) ID() string { return s.id }
-func (s debugSession) Cache() debug.Cache { return debugCache{s.cache} }
-func (s debugSession) Files() []*debug.File {
+func (s DebugSession) ID() string { return s.id }
+func (s DebugSession) Cache() debug.Cache { return debugCache{s.cache} }
+func (s DebugSession) Files() []*debug.File {
var files []*debug.File
seen := make(map[span.URI]*debug.File)
s.overlayMu.Lock()
@@ -42,7 +43,7 @@
return files
}
-func (s debugSession) File(hash string) *debug.File {
+func (s DebugSession) File(hash string) *debug.File {
s.overlayMu.Lock()
defer s.overlayMu.Unlock()
for _, overlay := range s.overlays {
diff --git a/internal/lsp/cache/errors.go b/internal/lsp/cache/errors.go
index f910fc1..16ddd50 100644
--- a/internal/lsp/cache/errors.go
+++ b/internal/lsp/cache/errors.go
@@ -177,11 +177,11 @@
func typeErrorRange(ctx context.Context, fset *token.FileSet, pkg *pkg, pos token.Pos) (span.Span, error) {
posn := fset.Position(pos)
- ph, _, err := findFileInPackage(pkg, span.FileURI(posn.Filename))
+ ph, _, err := source.FindFileInPackage(pkg, span.URIFromPath(posn.Filename))
if err != nil {
return span.Span{}, err
}
- _, m, _, err := ph.Cached()
+ _, _, m, _, err := ph.Cached()
if err != nil {
return span.Span{}, err
}
@@ -213,11 +213,11 @@
}
func scannerErrorRange(ctx context.Context, fset *token.FileSet, pkg *pkg, posn token.Position) (span.Span, error) {
- ph, _, err := findFileInPackage(pkg, span.FileURI(posn.Filename))
+ ph, _, err := source.FindFileInPackage(pkg, span.URIFromPath(posn.Filename))
if err != nil {
return span.Span{}, err
}
- file, _, _, err := ph.Cached()
+ file, _, _, _, err := ph.Cached()
if err != nil {
return span.Span{}, err
}
@@ -232,38 +232,17 @@
// spanToRange converts a span.Span to a protocol.Range,
// assuming that the span belongs to the package whose diagnostics are being computed.
func spanToRange(ctx context.Context, pkg *pkg, spn span.Span) (protocol.Range, error) {
- ph, _, err := findFileInPackage(pkg, spn.URI())
+ ph, _, err := source.FindFileInPackage(pkg, spn.URI())
if err != nil {
return protocol.Range{}, err
}
- _, m, _, err := ph.Cached()
+ _, _, m, _, err := ph.Cached()
if err != nil {
return protocol.Range{}, err
}
return m.Range(spn)
}
-func findFileInPackage(pkg source.Package, uri span.URI) (source.ParseGoHandle, source.Package, error) {
- queue := []source.Package{pkg}
- seen := make(map[string]bool)
-
- for len(queue) > 0 {
- pkg := queue[0]
- queue = queue[1:]
- seen[pkg.ID()] = true
-
- if f, err := pkg.File(uri); err == nil {
- return f, pkg, nil
- }
- for _, dep := range pkg.Imports() {
- if !seen[dep.ID()] {
- queue = append(queue, dep)
- }
- }
- }
- return nil, nil, errors.Errorf("no file for %s in package %s", uri, pkg.ID())
-}
-
// parseGoListError attempts to parse a standard `go list` error message
// by stripping off the trailing error message.
//
@@ -297,7 +276,7 @@
// Imports have quotation marks around them.
circImp := strconv.Quote(importList[1])
for _, ph := range pkg.compiledGoFiles {
- fh, _, _, err := ph.Parse(ctx)
+ fh, _, _, _, err := ph.Parse(ctx)
if err != nil {
continue
}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 7248a65..268bc6b 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -33,13 +33,15 @@
errors []packages.Error
deps []packageID
missingDeps map[packagePath]struct{}
+ module *packagesinternal.Module
// config is the *packages.Config associated with the loaded package.
config *packages.Config
}
-func (s *snapshot) load(ctx context.Context, scopes ...interface{}) ([]*metadata, error) {
+func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error {
var query []string
+ var containsDir bool // for logging
for _, scope := range scopes {
switch scope := scope.(type) {
case packagePath:
@@ -70,84 +72,31 @@
default:
panic(fmt.Sprintf("unknown scope type %T", scope))
}
+ switch scope.(type) {
+ case directoryURI, viewLoadScope:
+ containsDir = true
+ }
}
sort.Strings(query) // for determinism
ctx, done := trace.StartSpan(ctx, "cache.view.load", telemetry.Query.Of(query))
defer done()
- cfg := s.view.Config(ctx)
+ cfg := s.Config(ctx)
pkgs, err := s.view.loadPackages(cfg, query...)
// If the context was canceled, return early. Otherwise, we might be
// type-checking an incomplete result. Check the context directly,
// because go/packages adds extra information to the error.
if ctx.Err() != nil {
- return nil, ctx.Err()
+ return ctx.Err()
}
log.Print(ctx, "go/packages.Load", tag.Of("snapshot", s.ID()), tag.Of("query", query), tag.Of("packages", len(pkgs)))
if len(pkgs) == 0 {
- return nil, err
+ return err
}
- return s.updateMetadata(ctx, scopes, pkgs, cfg)
-}
-
-// shouldLoad reparses a file's package and import declarations to
-// determine if the file requires a metadata reload.
-func (c *cache) shouldLoad(ctx context.Context, s *snapshot, originalFH, currentFH source.FileHandle) bool {
- if originalFH == nil {
- return currentFH.Identity().Kind == source.Go
- }
- // If the file hasn't changed, there's no need to reload.
- if originalFH.Identity().String() == currentFH.Identity().String() {
- return false
- }
- // If a go.mod file's contents have changed, always invalidate metadata.
- if kind := originalFH.Identity().Kind; kind == source.Mod {
- return true
- }
- // Get the original and current parsed files in order to check package name and imports.
- original, _, _, originalErr := c.ParseGoHandle(originalFH, source.ParseHeader).Parse(ctx)
- current, _, _, currentErr := c.ParseGoHandle(currentFH, source.ParseHeader).Parse(ctx)
- if originalErr != nil || currentErr != nil {
- return (originalErr == nil) != (currentErr == nil)
- }
-
- // Check if the package's metadata has changed. The cases handled are:
- // 1. A package's name has changed
- // 2. A file's imports have changed
- if original.Name.Name != current.Name.Name {
- return true
- }
- // If the package's imports have increased, definitely re-run `go list`.
- if len(original.Imports) < len(current.Imports) {
- return true
- }
- importSet := make(map[string]struct{})
- for _, importSpec := range original.Imports {
- importSet[importSpec.Path.Value] = struct{}{}
- }
- // If any of the current imports were not in the original imports.
- for _, importSpec := range current.Imports {
- if _, ok := importSet[importSpec.Path.Value]; !ok {
- return true
- }
- }
- return false
-}
-
-func (s *snapshot) updateMetadata(ctx context.Context, scopes []interface{}, pkgs []*packages.Package, cfg *packages.Config) ([]*metadata, error) {
- var results []*metadata
for _, pkg := range pkgs {
- // Don't log output for full workspace packages.Loads.
- var containsDir bool
- for _, scope := range scopes {
- switch scope.(type) {
- case directoryURI, viewLoadScope:
- containsDir = true
- }
- }
if !containsDir || s.view.Options().VerboseOutput {
log.Print(ctx, "go/packages.Load", tag.Of("snapshot", s.ID()), tag.Of("package", pkg.PkgPath), tag.Of("files", pkg.CompiledGoFiles))
}
@@ -156,32 +105,36 @@
if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 {
continue
}
+ // Special case for the builtin package, as it has no dependencies.
+ if pkg.PkgPath == "builtin" {
+ if err := s.view.buildBuiltinPackage(ctx, pkg.GoFiles); err != nil {
+ return err
+ }
+ continue
+ }
// Skip test main packages.
if isTestMain(ctx, pkg, s.view.gocache) {
continue
}
// Set the metadata for this package.
- if err := s.updateImports(ctx, packagePath(pkg.PkgPath), pkg, cfg, map[packageID]struct{}{}); err != nil {
- return nil, err
+ m, err := s.setMetadata(ctx, packagePath(pkg.PkgPath), pkg, cfg, map[packageID]struct{}{})
+ if err != nil {
+ return err
}
- if m := s.getMetadata(packageID(pkg.ID)); m != nil {
- results = append(results, m)
+ if _, err := s.buildPackageHandle(ctx, m.id, source.ParseFull); err != nil {
+ return err
}
}
-
// Rebuild the import graph when the metadata is updated.
s.clearAndRebuildImportGraph()
- if len(results) == 0 {
- return nil, errors.Errorf("no metadata for %s", scopes)
- }
- return results, nil
+ return nil
}
-func (s *snapshot) updateImports(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) error {
+func (s *snapshot) setMetadata(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) (*metadata, error) {
id := packageID(pkg.ID)
if _, ok := seen[id]; ok {
- return errors.Errorf("import cycle detected: %q", id)
+ return nil, errors.Errorf("import cycle detected: %q", id)
}
// Recreate the metadata rather than reusing it to avoid locking.
m := &metadata{
@@ -192,21 +145,23 @@
typesSizes: pkg.TypesSizes,
errors: pkg.Errors,
config: cfg,
+ module: packagesinternal.GetModule(pkg),
}
for _, filename := range pkg.CompiledGoFiles {
- uri := span.FileURI(filename)
+ uri := span.URIFromPath(filename)
m.compiledGoFiles = append(m.compiledGoFiles, uri)
s.addID(uri, m.id)
}
for _, filename := range pkg.GoFiles {
- uri := span.FileURI(filename)
+ uri := span.URIFromPath(filename)
m.goFiles = append(m.goFiles, uri)
s.addID(uri, m.id)
}
- seen[id] = struct{}{}
- copied := make(map[packageID]struct{})
+ copied := map[packageID]struct{}{
+ id: struct{}{},
+ }
for k, v := range seen {
copied[k] = v
}
@@ -225,14 +180,41 @@
continue
}
if s.getMetadata(importID) == nil {
- if err := s.updateImports(ctx, importPkgPath, importPkg, cfg, copied); err != nil {
+ if _, err := s.setMetadata(ctx, importPkgPath, importPkg, cfg, copied); err != nil {
log.Error(ctx, "error in dependency", err)
}
}
}
+
// Add the metadata to the cache.
- s.setMetadata(m)
- return nil
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ // TODO: We should make sure not to set duplicate metadata,
+ // and instead panic here. This can be done by making sure not to
+ // reset metadata information for packages we've already seen.
+ if original, ok := s.metadata[m.id]; ok {
+ m = original
+ } else {
+ s.metadata[m.id] = m
+ }
+
+ // Set the workspace packages. If any of the package's files belong to the
+ // view, then the package is considered to be a workspace package.
+ for _, uri := range append(m.compiledGoFiles, m.goFiles...) {
+ // If the package's files are in this view, mark it as a workspace package.
+ if s.view.contains(uri) {
+ // A test variant of a package can only be loaded directly by loading
+ // the non-test variant with -test. Track the import path of the non-test variant.
+ if m.forTest != "" {
+ s.workspacePackages[m.id] = m.forTest
+ } else {
+ s.workspacePackages[m.id] = pkgPath
+ }
+ break
+ }
+ }
+ return m, nil
}
func isTestMain(ctx context.Context, pkg *packages.Package, gocache string) bool {
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
new file mode 100644
index 0000000..d730d6e
--- /dev/null
+++ b/internal/lsp/cache/mod.go
@@ -0,0 +1,631 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/gocommand"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/lsp/telemetry"
+ "golang.org/x/tools/internal/memoize"
+ "golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/telemetry/log"
+ "golang.org/x/tools/internal/telemetry/trace"
+ errors "golang.org/x/xerrors"
+)
+
+const (
+ ModTidyError = "go mod tidy"
+ SyntaxError = "syntax"
+)
+
+type modKey struct {
+ cfg string
+ gomod string
+ view string
+}
+
+type modTidyKey struct {
+ cfg string
+ gomod string
+ imports string
+ view string
+}
+
+type modHandle struct {
+ handle *memoize.Handle
+ file source.FileHandle
+ cfg *packages.Config
+}
+
+type modData struct {
+ memoize.NoCopy
+
+ // origfh is the file handle for the original go.mod file.
+ origfh source.FileHandle
+
+ // origParsedFile contains the parsed contents that are used to diff with
+ // the ideal contents.
+ origParsedFile *modfile.File
+
+ // origMapper is the column mapper for the original go.mod file.
+ origMapper *protocol.ColumnMapper
+
+ // idealParsedFile contains the parsed contents for the go.mod file
+ // after it has been "tidied".
+ idealParsedFile *modfile.File
+
+ // unusedDeps is the map containing the dependencies that are left after
+ // removing the ones that are identical in the original and ideal go.mods.
+ unusedDeps map[string]*modfile.Require
+
+ // missingDeps is the map containing the dependencies that are left after
+ // removing the ones that are identical in the original and ideal go.mods.
+ missingDeps map[string]*modfile.Require
+
+ // upgrades is a map of path->version that contains any upgrades for the go.mod.
+ upgrades map[string]string
+
+ // why is a map of path->explanation that contains all the "go mod why" contents
+ // for each require statement.
+ why map[string]string
+
+ // parseErrors are the errors that arise when we diff between a user's go.mod
+ // and the "tidied" go.mod.
+ parseErrors []source.Error
+
+ // err is any error that occurs while we are calculating the parseErrors.
+ err error
+}
+
+func (mh *modHandle) String() string {
+ return mh.File().Identity().URI.Filename()
+}
+
+func (mh *modHandle) File() source.FileHandle {
+ return mh.file
+}
+
+func (mh *modHandle) Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error) {
+ v := mh.handle.Get(ctx)
+ if v == nil {
+ return nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
+ }
+ data := v.(*modData)
+ return data.origParsedFile, data.origMapper, data.err
+}
+
+func (mh *modHandle) Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) {
+ v := mh.handle.Get(ctx)
+ if v == nil {
+ return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
+ }
+ data := v.(*modData)
+ return data.origParsedFile, data.origMapper, data.upgrades, data.err
+}
+
+func (mh *modHandle) Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) {
+ v := mh.handle.Get(ctx)
+ if v == nil {
+ return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
+ }
+ data := v.(*modData)
+ return data.origParsedFile, data.origMapper, data.why, data.err
+}
+
+func (s *snapshot) ModHandle(ctx context.Context, fh source.FileHandle) source.ModHandle {
+ uri := fh.Identity().URI
+ if handle := s.getModHandle(uri); handle != nil {
+ return handle
+ }
+
+ realURI, tempURI := s.view.ModFiles()
+ folder := s.View().Folder().Filename()
+ cfg := s.Config(ctx)
+
+ key := modKey{
+ cfg: hashConfig(cfg),
+ gomod: fh.Identity().String(),
+ view: folder,
+ }
+ h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
+ ctx, done := trace.StartSpan(ctx, "cache.ModHandle", telemetry.File.Of(uri))
+ defer done()
+
+ contents, _, err := fh.Read(ctx)
+ if err != nil {
+ return &modData{
+ err: err,
+ }
+ }
+ parsedFile, err := modfile.Parse(uri.Filename(), contents, nil)
+ if err != nil {
+ return &modData{
+ err: err,
+ }
+ }
+ data := &modData{
+ origfh: fh,
+ origParsedFile: parsedFile,
+ origMapper: &protocol.ColumnMapper{
+ URI: uri,
+ Converter: span.NewContentConverter(uri.Filename(), contents),
+ Content: contents,
+ },
+ }
+ // If the go.mod file is not the view's go.mod file, then we just want to parse.
+ if uri != realURI {
+ return data
+ }
+
+ // If we have a tempModfile, copy the real go.mod file content into the temp go.mod file.
+ if tempURI != "" {
+ if err := ioutil.WriteFile(tempURI.Filename(), contents, os.ModePerm); err != nil {
+ data.err = err
+ return data
+ }
+ }
+ // Only get dependency upgrades if the go.mod file is the same as the view's.
+ if err := dependencyUpgrades(ctx, cfg, folder, data); err != nil {
+ data.err = err
+ return data
+ }
+ // Only run "go mod why" if the go.mod file is the same as the view's.
+ if err := goModWhy(ctx, cfg, folder, data); err != nil {
+ data.err = err
+ return data
+ }
+ return data
+ })
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.modHandles[uri] = &modHandle{
+ handle: h,
+ file: fh,
+ cfg: cfg,
+ }
+ return s.modHandles[uri]
+}
+
+func goModWhy(ctx context.Context, cfg *packages.Config, folder string, data *modData) error {
+ if len(data.origParsedFile.Require) == 0 {
+ return nil
+ }
+ // Run "go mod why" on all the dependencies to get information about the usages.
+ inv := gocommand.Invocation{
+ Verb: "mod",
+ Args: []string{"why", "-m"},
+ BuildFlags: cfg.BuildFlags,
+ Env: cfg.Env,
+ WorkingDir: folder,
+ }
+ for _, req := range data.origParsedFile.Require {
+ inv.Args = append(inv.Args, req.Mod.Path)
+ }
+ stdout, err := inv.Run(ctx)
+ if err != nil {
+ return err
+ }
+ whyList := strings.Split(stdout.String(), "\n\n")
+ if len(whyList) <= 1 || len(whyList) > len(data.origParsedFile.Require) {
+ return nil
+ }
+ data.why = make(map[string]string)
+ for i, req := range data.origParsedFile.Require {
+ data.why[req.Mod.Path] = whyList[i]
+ }
+ return nil
+}
+
+func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string, data *modData) error {
+ if len(data.origParsedFile.Require) == 0 {
+ return nil
+ }
+ // Run "go list -u -m all" to be able to see which deps can be upgraded.
+ inv := gocommand.Invocation{
+ Verb: "list",
+ Args: []string{"-u", "-m", "all"},
+ BuildFlags: cfg.BuildFlags,
+ Env: cfg.Env,
+ WorkingDir: folder,
+ }
+ stdout, err := inv.Run(ctx)
+ if err != nil {
+ return err
+ }
+ upgradesList := strings.Split(stdout.String(), "\n")
+ if len(upgradesList) <= 1 {
+ return nil
+ }
+ data.upgrades = make(map[string]string)
+ for _, upgrade := range upgradesList[1:] {
+ // Example: "github.com/x/tools v1.1.0 [v1.2.0]"
+ info := strings.Split(upgrade, " ")
+ if len(info) < 3 {
+ continue
+ }
+ dep, version := info[0], info[2]
+ latest := version[1:] // remove the "["
+ latest = strings.TrimSuffix(latest, "]") // remove the "]"
+ data.upgrades[dep] = latest
+ }
+ return nil
+}
+
+func (mh *modHandle) Tidy(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]*modfile.Require, []source.Error, error) {
+ v := mh.handle.Get(ctx)
+ if v == nil {
+ return nil, nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
+ }
+ data := v.(*modData)
+ return data.origParsedFile, data.origMapper, data.missingDeps, data.parseErrors, data.err
+}
+
+func (s *snapshot) ModTidyHandle(ctx context.Context, realfh source.FileHandle) (source.ModTidyHandle, error) {
+ realURI, tempURI := s.view.ModFiles()
+ cfg := s.Config(ctx)
+ options := s.View().Options()
+ folder := s.View().Folder().Filename()
+
+ wsPackages, err := s.WorkspacePackages(ctx)
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+ if err != nil {
+ return nil, err
+ }
+ imports, err := hashImports(ctx, wsPackages)
+ if err != nil {
+ return nil, err
+ }
+ key := modTidyKey{
+ view: folder,
+ imports: imports,
+ gomod: realfh.Identity().Identifier,
+ cfg: hashConfig(cfg),
+ }
+ h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
+ data := &modData{}
+
+ // Check the case when the tempModfile flag is turned off.
+ if realURI == "" || tempURI == "" {
+ return data
+ }
+
+ ctx, done := trace.StartSpan(ctx, "cache.ModTidyHandle", telemetry.File.Of(realURI))
+ defer done()
+
+ realContents, _, err := realfh.Read(ctx)
+ if err != nil {
+ data.err = err
+ return data
+ }
+ realMapper := &protocol.ColumnMapper{
+ URI: realURI,
+ Converter: span.NewContentConverter(realURI.Filename(), realContents),
+ Content: realContents,
+ }
+ origParsedFile, err := modfile.Parse(realURI.Filename(), realContents, nil)
+ if err != nil {
+ if parseErr, err := extractModParseErrors(ctx, realURI, realMapper, err, realContents); err == nil {
+ data.parseErrors = []source.Error{parseErr}
+ return data
+ }
+ data.err = err
+ return data
+ }
+
+ // Copy the real go.mod file content into the temp go.mod file.
+ if err := ioutil.WriteFile(tempURI.Filename(), realContents, os.ModePerm); err != nil {
+ data.err = err
+ return data
+ }
+
+ // We want to run "go mod tidy" to be able to diff between the real and the temp files.
+ inv := gocommand.Invocation{
+ Verb: "mod",
+ Args: []string{"tidy"},
+ BuildFlags: cfg.BuildFlags,
+ Env: cfg.Env,
+ WorkingDir: folder,
+ }
+ if _, err := inv.Run(ctx); err != nil {
+ // Ignore concurrency errors here.
+ if !modConcurrencyError.MatchString(err.Error()) {
+ data.err = err
+ return data
+ }
+ }
+
+ // Go directly to disk to get the temporary mod file, since it is always on disk.
+ tempContents, err := ioutil.ReadFile(tempURI.Filename())
+ if err != nil {
+ data.err = err
+ return data
+ }
+ idealParsedFile, err := modfile.Parse(tempURI.Filename(), tempContents, nil)
+ if err != nil {
+ // We do not need to worry about the temporary file's parse errors since it has been "tidied".
+ data.err = err
+ return data
+ }
+
+ data = &modData{
+ origfh: realfh,
+ origParsedFile: origParsedFile,
+ origMapper: realMapper,
+ idealParsedFile: idealParsedFile,
+ unusedDeps: make(map[string]*modfile.Require, len(origParsedFile.Require)),
+ missingDeps: make(map[string]*modfile.Require, len(idealParsedFile.Require)),
+ }
+ // Get the dependencies that are different between the original and ideal mod files.
+ for _, req := range origParsedFile.Require {
+ data.unusedDeps[req.Mod.Path] = req
+ }
+ for _, req := range idealParsedFile.Require {
+ origDep := data.unusedDeps[req.Mod.Path]
+ if origDep != nil && origDep.Indirect == req.Indirect {
+ delete(data.unusedDeps, req.Mod.Path)
+ } else {
+ data.missingDeps[req.Mod.Path] = req
+ }
+ }
+ data.parseErrors, data.err = modRequireErrors(ctx, options, data)
+
+ for _, req := range data.missingDeps {
+ if data.unusedDeps[req.Mod.Path] != nil {
+ delete(data.missingDeps, req.Mod.Path)
+ }
+ }
+ return data
+ })
+ return &modHandle{
+ handle: h,
+ file: realfh,
+ cfg: cfg,
+ }, nil
+}
+
+// extractModParseErrors processes the raw errors returned by modfile.Parse,
+// extracting the filenames and line numbers that correspond to the errors.
+func extractModParseErrors(ctx context.Context, uri span.URI, m *protocol.ColumnMapper, parseErr error, content []byte) (source.Error, error) {
+ re := regexp.MustCompile(`.*:([\d]+): (.+)`)
+ matches := re.FindStringSubmatch(strings.TrimSpace(parseErr.Error()))
+ if len(matches) < 3 {
+ log.Error(ctx, "could not parse golang/x/mod error message", parseErr)
+ return source.Error{}, parseErr
+ }
+ line, err := strconv.Atoi(matches[1])
+ if err != nil {
+ return source.Error{}, parseErr
+ }
+ lines := strings.Split(string(content), "\n")
+ if len(lines) <= line {
+ return source.Error{}, errors.Errorf("could not parse goland/x/mod error message, line number out of range")
+ }
+ // The error returned from the modfile package only returns a line number,
+ // so we assume that the diagnostic should be for the entire line.
+ endOfLine := len(lines[line-1])
+ sOffset, err := m.Converter.ToOffset(line, 0)
+ if err != nil {
+ return source.Error{}, err
+ }
+ eOffset, err := m.Converter.ToOffset(line, endOfLine)
+ if err != nil {
+ return source.Error{}, err
+ }
+ spn := span.New(uri, span.NewPoint(line, 0, sOffset), span.NewPoint(line, endOfLine, eOffset))
+ rng, err := m.Range(spn)
+ if err != nil {
+ return source.Error{}, err
+ }
+ return source.Error{
+ Category: SyntaxError,
+ Message: matches[2],
+ Range: rng,
+ URI: uri,
+ }, nil
+}
+
+// modRequireErrors extracts the errors that occur on the require directives.
+// It checks for directness issues and unused dependencies.
+func modRequireErrors(ctx context.Context, options source.Options, data *modData) ([]source.Error, error) {
+ var errors []source.Error
+ for dep, req := range data.unusedDeps {
+ if req.Syntax == nil {
+ continue
+ }
+ // Handle dependencies that are incorrectly labeled indirect and vice versa.
+ if data.missingDeps[dep] != nil && req.Indirect != data.missingDeps[dep].Indirect {
+ directErr, err := modDirectnessErrors(ctx, options, data, req)
+ if err != nil {
+ return nil, err
+ }
+ errors = append(errors, directErr)
+ }
+ // Handle unused dependencies.
+ if data.missingDeps[dep] == nil {
+ rng, err := rangeFromPositions(data.origfh.Identity().URI, data.origMapper, req.Syntax.Start, req.Syntax.End)
+ if err != nil {
+ return nil, err
+ }
+ edits, err := dropDependencyEdits(ctx, options, data, req)
+ if err != nil {
+ return nil, err
+ }
+ errors = append(errors, source.Error{
+ Category: ModTidyError,
+ Message: fmt.Sprintf("%s is not used in this module.", dep),
+ Range: rng,
+ URI: data.origfh.Identity().URI,
+ SuggestedFixes: []source.SuggestedFix{{
+ Title: fmt.Sprintf("Remove dependency: %s", dep),
+ Edits: map[span.URI][]protocol.TextEdit{data.origfh.Identity().URI: edits},
+ }},
+ })
+ }
+ }
+ return errors, nil
+}
+
+// modDirectnessErrors extracts errors when a dependency is labeled indirect when it should be direct and vice versa.
+func modDirectnessErrors(ctx context.Context, options source.Options, data *modData, req *modfile.Require) (source.Error, error) {
+ rng, err := rangeFromPositions(data.origfh.Identity().URI, data.origMapper, req.Syntax.Start, req.Syntax.End)
+ if err != nil {
+ return source.Error{}, err
+ }
+ if req.Indirect {
+ // If the dependency should be direct, just highlight the // indirect.
+ if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
+ end := comments.Suffix[0].Start
+ end.LineRune += len(comments.Suffix[0].Token)
+ end.Byte += len([]byte(comments.Suffix[0].Token))
+ rng, err = rangeFromPositions(data.origfh.Identity().URI, data.origMapper, comments.Suffix[0].Start, end)
+ if err != nil {
+ return source.Error{}, err
+ }
+ }
+ edits, err := changeDirectnessEdits(ctx, options, data, req, false)
+ if err != nil {
+ return source.Error{}, err
+ }
+ return source.Error{
+ Category: ModTidyError,
+ Message: fmt.Sprintf("%s should be a direct dependency.", req.Mod.Path),
+ Range: rng,
+ URI: data.origfh.Identity().URI,
+ SuggestedFixes: []source.SuggestedFix{{
+ Title: fmt.Sprintf("Make %s direct", req.Mod.Path),
+ Edits: map[span.URI][]protocol.TextEdit{data.origfh.Identity().URI: edits},
+ }},
+ }, nil
+ }
+ // If the dependency should be indirect, add the // indirect.
+ edits, err := changeDirectnessEdits(ctx, options, data, req, true)
+ if err != nil {
+ return source.Error{}, err
+ }
+ return source.Error{
+ Category: ModTidyError,
+ Message: fmt.Sprintf("%s should be an indirect dependency.", req.Mod.Path),
+ Range: rng,
+ URI: data.origfh.Identity().URI,
+ SuggestedFixes: []source.SuggestedFix{{
+ Title: fmt.Sprintf("Make %s indirect", req.Mod.Path),
+ Edits: map[span.URI][]protocol.TextEdit{data.origfh.Identity().URI: edits},
+ }},
+ }, nil
+}
+
+// dropDependencyEdits gets the edits needed to remove the dependency from the go.mod file.
+// As an example, this function will codify the edits needed to convert the before go.mod file to the after.
+// Before:
+// module t
+//
+// go 1.11
+//
+// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee
+// After:
+// module t
+//
+// go 1.11
+func dropDependencyEdits(ctx context.Context, options source.Options, data *modData, req *modfile.Require) ([]protocol.TextEdit, error) {
+ if err := data.origParsedFile.DropRequire(req.Mod.Path); err != nil {
+ return nil, err
+ }
+ data.origParsedFile.Cleanup()
+ newContents, err := data.origParsedFile.Format()
+ if err != nil {
+ return nil, err
+ }
+ // Reset the *modfile.File back to before we dropped the dependency.
+ data.origParsedFile.AddNewRequire(req.Mod.Path, req.Mod.Version, req.Indirect)
+ // Calculate the edits to be made due to the change.
+ diff := options.ComputeEdits(data.origfh.Identity().URI, string(data.origMapper.Content), string(newContents))
+ edits, err := source.ToProtocolEdits(data.origMapper, diff)
+ if err != nil {
+ return nil, err
+ }
+ return edits, nil
+}
+
+// changeDirectnessEdits gets the edits needed to change an indirect dependency to direct and vice versa.
+// As an example, this function will codify the edits needed to convert the before go.mod file to the after.
+// Before:
+// module t
+//
+// go 1.11
+//
+// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee
+// After:
+// module t
+//
+// go 1.11
+//
+// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee // indirect
+func changeDirectnessEdits(ctx context.Context, options source.Options, data *modData, req *modfile.Require, indirect bool) ([]protocol.TextEdit, error) {
+ var newReq []*modfile.Require
+ prevIndirect := false
+ // Change the directness in the matching require statement.
+ for _, r := range data.origParsedFile.Require {
+ if req.Mod.Path == r.Mod.Path {
+ prevIndirect = req.Indirect
+ req.Indirect = indirect
+ }
+ newReq = append(newReq, r)
+ }
+ data.origParsedFile.SetRequire(newReq)
+ data.origParsedFile.Cleanup()
+ newContents, err := data.origParsedFile.Format()
+ if err != nil {
+ return nil, err
+ }
+ // Change the dependency back to the way it was before we got the newContents.
+ for _, r := range data.origParsedFile.Require {
+ if req.Mod.Path == r.Mod.Path {
+ req.Indirect = prevIndirect
+ }
+ newReq = append(newReq, r)
+ }
+ data.origParsedFile.SetRequire(newReq)
+ // Calculate the edits to be made due to the change.
+ diff := options.ComputeEdits(data.origfh.Identity().URI, string(data.origMapper.Content), string(newContents))
+ edits, err := source.ToProtocolEdits(data.origMapper, diff)
+ if err != nil {
+ return nil, err
+ }
+ return edits, nil
+}
+
+func rangeFromPositions(uri span.URI, m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
+ line, col, err := m.Converter.ToPosition(s.Byte)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ start := span.NewPoint(line, col, s.Byte)
+
+ line, col, err = m.Converter.ToPosition(e.Byte)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ end := span.NewPoint(line, col, e.Byte)
+
+ spn := span.New(uri, start, end)
+ rng, err := m.Range(spn)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ return rng, nil
+}
diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go
deleted file mode 100644
index e301245..0000000
--- a/internal/lsp/cache/mod_tidy.go
+++ /dev/null
@@ -1,428 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package cache
-
-import (
- "context"
- "fmt"
- "io/ioutil"
- "os"
- "regexp"
- "strconv"
- "strings"
-
- "golang.org/x/mod/modfile"
- "golang.org/x/tools/go/packages"
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/lsp/telemetry"
- "golang.org/x/tools/internal/memoize"
- "golang.org/x/tools/internal/span"
- "golang.org/x/tools/internal/telemetry/log"
- "golang.org/x/tools/internal/telemetry/trace"
- errors "golang.org/x/xerrors"
-)
-
-const ModTidyError = "go mod tidy"
-const SyntaxError = "syntax"
-
-type parseModKey struct {
- snapshot source.Snapshot
- cfg string
-}
-
-type modTidyHandle struct {
- handle *memoize.Handle
- file source.FileHandle
- cfg *packages.Config
-}
-
-type modTidyData struct {
- memoize.NoCopy
-
- // origfh is the file handle for the original go.mod file.
- origfh source.FileHandle
-
- // origParsedFile contains the parsed contents that are used to diff with
- // the ideal contents.
- origParsedFile *modfile.File
-
- // origMapper is the column mapper for the original go.mod file.
- origMapper *protocol.ColumnMapper
-
- // idealParsedFile contains the parsed contents for the go.mod file
- // after it has been "tidied".
- idealParsedFile *modfile.File
-
- // unusedDeps is the map containing the dependencies that are left after
- // removing the ones that are identical in the original and ideal go.mods.
- unusedDeps map[string]*modfile.Require
-
- // missingDeps is the map containing the dependencies that are left after
- // removing the ones that are identical in the original and ideal go.mods.
- missingDeps map[string]*modfile.Require
-
- // parseErrors are the errors that arise when we diff between a user's go.mod
- // and the "tidied" go.mod.
- parseErrors []source.Error
-
- // err is any error that occurs while we are calculating the parseErrors.
- err error
-}
-
-func (mth *modTidyHandle) String() string {
- return mth.File().Identity().URI.Filename()
-}
-
-func (mth *modTidyHandle) File() source.FileHandle {
- return mth.file
-}
-
-func (mth *modTidyHandle) Tidy(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]*modfile.Require, []source.Error, error) {
- v := mth.handle.Get(ctx)
- if v == nil {
- return nil, nil, nil, nil, errors.Errorf("no parsed file for %s", mth.File().Identity().URI)
- }
- data := v.(*modTidyData)
- return data.origParsedFile, data.origMapper, data.missingDeps, data.parseErrors, data.err
-}
-
-func (s *snapshot) ModTidyHandle(ctx context.Context, realfh source.FileHandle) source.ModTidyHandle {
- realURI, tempURI := s.view.ModFiles()
- cfg := s.View().Config(ctx)
- options := s.View().Options()
- folder := s.View().Folder().Filename()
-
- key := parseModKey{
- snapshot: s,
- cfg: hashConfig(cfg),
- }
- h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
- data := &modTidyData{}
-
- // Check the case when the tempModfile flag is turned off.
- if realURI == "" || tempURI == "" {
- return data
- }
-
- ctx, done := trace.StartSpan(ctx, "cache.ModTidyHandle", telemetry.File.Of(realURI))
- defer done()
-
- // Copy the real go.mod file content into the temp go.mod file.
- realContents, _, err := realfh.Read(ctx)
- if err != nil {
- data.err = err
- return data
- }
- if err := ioutil.WriteFile(tempURI.Filename(), realContents, os.ModePerm); err != nil {
- data.err = err
- return data
- }
-
- // We want to run "go mod tidy" to be able to diff between the real and the temp files.
- args := append([]string{"mod", "tidy"}, cfg.BuildFlags...)
- if _, err := source.InvokeGo(ctx, folder, cfg.Env, args...); err != nil {
- // Ignore parse errors here. They'll be handled below.
- if !strings.Contains(err.Error(), "errors parsing go.mod") {
- data.err = err
- return data
- }
- }
-
- realMapper := &protocol.ColumnMapper{
- URI: realURI,
- Converter: span.NewContentConverter(realURI.Filename(), realContents),
- Content: realContents,
- }
- origParsedFile, err := modfile.Parse(realURI.Filename(), realContents, nil)
- if err != nil {
- if parseErr, err := extractModParseErrors(ctx, realURI, realMapper, err, realContents); err == nil {
- data.parseErrors = []source.Error{parseErr}
- return data
- }
- data.err = err
- return data
- }
-
- // Go directly to disk to get the temporary mod file, since it is always on disk.
- tempContents, err := ioutil.ReadFile(tempURI.Filename())
- if err != nil {
- data.err = err
- return data
- }
- idealParsedFile, err := modfile.Parse(tempURI.Filename(), tempContents, nil)
- if err != nil {
- // We do not need to worry about the temporary file's parse errors since it has been "tidied".
- data.err = err
- return data
- }
-
- data = &modTidyData{
- origfh: realfh,
- origParsedFile: origParsedFile,
- origMapper: realMapper,
- idealParsedFile: idealParsedFile,
- unusedDeps: make(map[string]*modfile.Require, len(origParsedFile.Require)),
- missingDeps: make(map[string]*modfile.Require, len(idealParsedFile.Require)),
- }
- // Get the dependencies that are different between the original and ideal mod files.
- for _, req := range origParsedFile.Require {
- data.unusedDeps[req.Mod.Path] = req
- }
- for _, req := range idealParsedFile.Require {
- origDep := data.unusedDeps[req.Mod.Path]
- if origDep != nil && origDep.Indirect == req.Indirect {
- delete(data.unusedDeps, req.Mod.Path)
- } else {
- data.missingDeps[req.Mod.Path] = req
- }
- }
- data.parseErrors, data.err = modRequireErrors(ctx, options, data)
-
- for _, req := range data.missingDeps {
- if data.unusedDeps[req.Mod.Path] != nil {
- delete(data.missingDeps, req.Mod.Path)
- }
- }
- return data
- })
- return &modTidyHandle{
- handle: h,
- file: realfh,
- cfg: cfg,
- }
-}
-
-// extractModParseErrors processes the raw errors returned by modfile.Parse,
-// extracting the filenames and line numbers that correspond to the errors.
-func extractModParseErrors(ctx context.Context, uri span.URI, m *protocol.ColumnMapper, parseErr error, content []byte) (source.Error, error) {
- re := regexp.MustCompile(`.*:([\d]+): (.+)`)
- matches := re.FindStringSubmatch(strings.TrimSpace(parseErr.Error()))
- if len(matches) < 3 {
- log.Error(ctx, "could not parse golang/x/mod error message", parseErr)
- return source.Error{}, parseErr
- }
- line, err := strconv.Atoi(matches[1])
- if err != nil {
- return source.Error{}, parseErr
- }
- lines := strings.Split(string(content), "\n")
- if len(lines) <= line {
- return source.Error{}, errors.Errorf("could not parse goland/x/mod error message, line number out of range")
- }
- // The error returned from the modfile package only returns a line number,
- // so we assume that the diagnostic should be for the entire line.
- endOfLine := len(lines[line-1])
- sOffset, err := m.Converter.ToOffset(line, 0)
- if err != nil {
- return source.Error{}, err
- }
- eOffset, err := m.Converter.ToOffset(line, endOfLine)
- if err != nil {
- return source.Error{}, err
- }
- spn := span.New(uri, span.NewPoint(line, 0, sOffset), span.NewPoint(line, endOfLine, eOffset))
- rng, err := m.Range(spn)
- if err != nil {
- return source.Error{}, err
- }
- return source.Error{
- Category: SyntaxError,
- Message: matches[2],
- Range: rng,
- URI: uri,
- }, nil
-}
-
-// modRequireErrors extracts the errors that occur on the require directives.
-// It checks for directness issues and unused dependencies.
-func modRequireErrors(ctx context.Context, options source.Options, modData *modTidyData) ([]source.Error, error) {
- var errors []source.Error
- for dep, req := range modData.unusedDeps {
- if req.Syntax == nil {
- continue
- }
- // Handle dependencies that are incorrectly labeled indirect and vice versa.
- if modData.missingDeps[dep] != nil && req.Indirect != modData.missingDeps[dep].Indirect {
- directErr, err := modDirectnessErrors(ctx, options, modData, req)
- if err != nil {
- return nil, err
- }
- errors = append(errors, directErr)
- }
- // Handle unused dependencies.
- if modData.missingDeps[dep] == nil {
- rng, err := rangeFromPositions(modData.origfh.Identity().URI, modData.origMapper, req.Syntax.Start, req.Syntax.End)
- if err != nil {
- return nil, err
- }
- edits, err := dropDependencyEdits(ctx, options, modData, req)
- if err != nil {
- return nil, err
- }
- errors = append(errors, source.Error{
- Category: ModTidyError,
- Message: fmt.Sprintf("%s is not used in this module.", dep),
- Range: rng,
- URI: modData.origfh.Identity().URI,
- SuggestedFixes: []source.SuggestedFix{{
- Title: fmt.Sprintf("Remove dependency: %s", dep),
- Edits: map[span.URI][]protocol.TextEdit{modData.origfh.Identity().URI: edits},
- }},
- })
- }
- }
- return errors, nil
-}
-
-// modDirectnessErrors extracts errors when a dependency is labeled indirect when it should be direct and vice versa.
-func modDirectnessErrors(ctx context.Context, options source.Options, modData *modTidyData, req *modfile.Require) (source.Error, error) {
- rng, err := rangeFromPositions(modData.origfh.Identity().URI, modData.origMapper, req.Syntax.Start, req.Syntax.End)
- if err != nil {
- return source.Error{}, err
- }
- if req.Indirect {
- // If the dependency should be direct, just highlight the // indirect.
- if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
- end := comments.Suffix[0].Start
- end.LineRune += len(comments.Suffix[0].Token)
- end.Byte += len([]byte(comments.Suffix[0].Token))
- rng, err = rangeFromPositions(modData.origfh.Identity().URI, modData.origMapper, comments.Suffix[0].Start, end)
- if err != nil {
- return source.Error{}, err
- }
- }
- edits, err := changeDirectnessEdits(ctx, options, modData, req, false)
- if err != nil {
- return source.Error{}, err
- }
- return source.Error{
- Category: ModTidyError,
- Message: fmt.Sprintf("%s should be a direct dependency.", req.Mod.Path),
- Range: rng,
- URI: modData.origfh.Identity().URI,
- SuggestedFixes: []source.SuggestedFix{{
- Title: fmt.Sprintf("Make %s direct", req.Mod.Path),
- Edits: map[span.URI][]protocol.TextEdit{modData.origfh.Identity().URI: edits},
- }},
- }, nil
- }
- // If the dependency should be indirect, add the // indirect.
- edits, err := changeDirectnessEdits(ctx, options, modData, req, true)
- if err != nil {
- return source.Error{}, err
- }
- return source.Error{
- Category: ModTidyError,
- Message: fmt.Sprintf("%s should be an indirect dependency.", req.Mod.Path),
- Range: rng,
- URI: modData.origfh.Identity().URI,
- SuggestedFixes: []source.SuggestedFix{{
- Title: fmt.Sprintf("Make %s indirect", req.Mod.Path),
- Edits: map[span.URI][]protocol.TextEdit{modData.origfh.Identity().URI: edits},
- }},
- }, nil
-}
-
-// dropDependencyEdits gets the edits needed to remove the dependency from the go.mod file.
-// As an example, this function will codify the edits needed to convert the before go.mod file to the after.
-// Before:
-// module t
-//
-// go 1.11
-//
-// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee
-// After:
-// module t
-//
-// go 1.11
-func dropDependencyEdits(ctx context.Context, options source.Options, modData *modTidyData, req *modfile.Require) ([]protocol.TextEdit, error) {
- if err := modData.origParsedFile.DropRequire(req.Mod.Path); err != nil {
- return nil, err
- }
- modData.origParsedFile.Cleanup()
- newContents, err := modData.origParsedFile.Format()
- if err != nil {
- return nil, err
- }
- // Reset the *modfile.File back to before we dropped the dependency.
- modData.origParsedFile.AddNewRequire(req.Mod.Path, req.Mod.Version, req.Indirect)
- // Calculate the edits to be made due to the change.
- diff := options.ComputeEdits(modData.origfh.Identity().URI, string(modData.origMapper.Content), string(newContents))
- edits, err := source.ToProtocolEdits(modData.origMapper, diff)
- if err != nil {
- return nil, err
- }
- return edits, nil
-}
-
-// changeDirectnessEdits gets the edits needed to change an indirect dependency to direct and vice versa.
-// As an example, this function will codify the edits needed to convert the before go.mod file to the after.
-// Before:
-// module t
-//
-// go 1.11
-//
-// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee
-// After:
-// module t
-//
-// go 1.11
-//
-// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee // indirect
-func changeDirectnessEdits(ctx context.Context, options source.Options, modData *modTidyData, req *modfile.Require, indirect bool) ([]protocol.TextEdit, error) {
- var newReq []*modfile.Require
- prevIndirect := false
- // Change the directness in the matching require statement.
- for _, r := range modData.origParsedFile.Require {
- if req.Mod.Path == r.Mod.Path {
- prevIndirect = req.Indirect
- req.Indirect = indirect
- }
- newReq = append(newReq, r)
- }
- modData.origParsedFile.SetRequire(newReq)
- modData.origParsedFile.Cleanup()
- newContents, err := modData.origParsedFile.Format()
- if err != nil {
- return nil, err
- }
- // Change the dependency back to the way it was before we got the newContents.
- for _, r := range modData.origParsedFile.Require {
- if req.Mod.Path == r.Mod.Path {
- req.Indirect = prevIndirect
- }
- newReq = append(newReq, r)
- }
- modData.origParsedFile.SetRequire(newReq)
- // Calculate the edits to be made due to the change.
- diff := options.ComputeEdits(modData.origfh.Identity().URI, string(modData.origMapper.Content), string(newContents))
- edits, err := source.ToProtocolEdits(modData.origMapper, diff)
- if err != nil {
- return nil, err
- }
- return edits, nil
-}
-
-func rangeFromPositions(uri span.URI, m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
- line, col, err := m.Converter.ToPosition(s.Byte)
- if err != nil {
- return protocol.Range{}, err
- }
- start := span.NewPoint(line, col, s.Byte)
-
- line, col, err = m.Converter.ToPosition(e.Byte)
- if err != nil {
- return protocol.Range{}, err
- }
- end := span.NewPoint(line, col, e.Byte)
-
- spn := span.New(uri, start, end)
- rng, err := m.Range(spn)
- if err != nil {
- return protocol.Range{}, err
- }
- return rng, nil
-}
diff --git a/internal/lsp/cache/overlay.go b/internal/lsp/cache/overlay.go
deleted file mode 100644
index dfe8687..0000000
--- a/internal/lsp/cache/overlay.go
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package cache
-
-import (
- "context"
-
- "golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
- errors "golang.org/x/xerrors"
-)
-
-type overlay struct {
- session *session
- uri span.URI
- text []byte
- hash string
- version float64
- kind source.FileKind
-
- // saved is true if a file has been saved on disk,
- // and therefore does not need to be part of the overlay sent to go/packages.
- saved bool
-}
-
-func (o *overlay) FileSystem() source.FileSystem {
- return o.session
-}
-
-func (o *overlay) Identity() source.FileIdentity {
- return source.FileIdentity{
- URI: o.uri,
- Identifier: o.hash,
- Version: o.version,
- Kind: o.kind,
- }
-}
-func (o *overlay) Read(ctx context.Context) ([]byte, string, error) {
- return o.text, o.hash, nil
-}
-
-func (s *session) updateOverlay(ctx context.Context, c source.FileModification) error {
- // Make sure that the file was not changed on disk.
- if c.OnDisk {
- return errors.Errorf("updateOverlay called for an on-disk change: %s", c.URI)
- }
-
- s.overlayMu.Lock()
- defer s.overlayMu.Unlock()
-
- o, ok := s.overlays[c.URI]
-
- // Determine the file kind on open, otherwise, assume it has been cached.
- var kind source.FileKind
- switch c.Action {
- case source.Open:
- kind = source.DetectLanguage(c.LanguageID, c.URI.Filename())
- default:
- if !ok {
- return errors.Errorf("updateOverlay: modifying unopened overlay %v", c.URI)
- }
- kind = o.kind
- }
- if kind == source.UnknownKind {
- return errors.Errorf("updateOverlay: unknown file kind for %s", c.URI)
- }
-
- // Closing a file just deletes its overlay.
- if c.Action == source.Close {
- delete(s.overlays, c.URI)
- return nil
- }
-
- // If the file is on disk, check if its content is the same as the overlay.
- text := c.Text
- if text == nil {
- text = o.text
- }
- hash := hashContents(text)
- var sameContentOnDisk bool
- switch c.Action {
- case source.Open:
- _, h, err := s.cache.GetFile(c.URI).Read(ctx)
- sameContentOnDisk = (err == nil && h == hash)
- case source.Save:
- // Make sure the version and content (if present) is the same.
- if o.version != c.Version {
- return errors.Errorf("updateOverlay: saving %s at version %v, currently at %v", c.URI, c.Version, o.version)
- }
- if c.Text != nil && o.hash != hash {
- return errors.Errorf("updateOverlay: overlay %s changed on save", c.URI)
- }
- sameContentOnDisk = true
- }
- s.overlays[c.URI] = &overlay{
- session: s,
- uri: c.URI,
- version: c.Version,
- text: text,
- kind: kind,
- hash: hash,
- saved: sameContentOnDisk,
- }
- return nil
-}
-
-func (s *session) readOverlay(uri span.URI) *overlay {
- s.overlayMu.Lock()
- defer s.overlayMu.Unlock()
-
- // We might have the content saved in an overlay.
- if overlay, ok := s.overlays[uri]; ok {
- return overlay
- }
- return nil
-}
-
-func (s *session) buildOverlay() map[string][]byte {
- s.overlayMu.Lock()
- defer s.overlayMu.Unlock()
-
- overlays := make(map[string][]byte)
- for uri, overlay := range s.overlays {
- // TODO(rstambler): Make sure not to send overlays outside of the current view.
- if overlay.saved {
- continue
- }
- overlays[uri.Filename()] = overlay.text
- }
- return overlays
-}
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index 9031fc8..8ddd56e 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -18,7 +18,6 @@
"golang.org/x/tools/internal/lsp/telemetry"
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/span"
- "golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/trace"
errors "golang.org/x/xerrors"
)
@@ -41,22 +40,21 @@
type parseGoData struct {
memoize.NoCopy
+ src []byte
ast *ast.File
parseError error // errors associated with parsing the file
mapper *protocol.ColumnMapper
err error
}
-func (c *cache) ParseGoHandle(fh source.FileHandle, mode source.ParseMode) source.ParseGoHandle {
+func (c *Cache) ParseGoHandle(fh source.FileHandle, mode source.ParseMode) source.ParseGoHandle {
key := parseKey{
file: fh.Identity(),
mode: mode,
}
fset := c.fset
h := c.store.Bind(key, func(ctx context.Context) interface{} {
- data := &parseGoData{}
- data.ast, data.mapper, data.parseError, data.err = parseGo(ctx, fset, fh, mode)
- return data
+ return parseGo(ctx, fset, fh, mode)
})
return &parseGoHandle{
handle: h,
@@ -77,22 +75,22 @@
return pgh.mode
}
-func (pgh *parseGoHandle) Parse(ctx context.Context) (*ast.File, *protocol.ColumnMapper, error, error) {
+func (pgh *parseGoHandle) Parse(ctx context.Context) (*ast.File, []byte, *protocol.ColumnMapper, error, error) {
v := pgh.handle.Get(ctx)
if v == nil {
- return nil, nil, nil, errors.Errorf("no parsed file for %s", pgh.File().Identity().URI)
+ return nil, nil, nil, nil, errors.Errorf("no parsed file for %s", pgh.File().Identity().URI)
}
data := v.(*parseGoData)
- return data.ast, data.mapper, data.parseError, data.err
+ return data.ast, data.src, data.mapper, data.parseError, data.err
}
-func (pgh *parseGoHandle) Cached() (*ast.File, *protocol.ColumnMapper, error, error) {
+func (pgh *parseGoHandle) Cached() (*ast.File, []byte, *protocol.ColumnMapper, error, error) {
v := pgh.handle.Cached()
if v == nil {
- return nil, nil, nil, errors.Errorf("no cached AST for %s", pgh.file.Identity().URI)
+ return nil, nil, nil, nil, errors.Errorf("no cached AST for %s", pgh.file.Identity().URI)
}
data := v.(*parseGoData)
- return data.ast, data.mapper, data.parseError, data.err
+ return data.ast, data.src, data.mapper, data.parseError, data.err
}
func hashParseKey(ph source.ParseGoHandle) string {
@@ -110,16 +108,16 @@
return hashContents(b.Bytes())
}
-func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode source.ParseMode) (file *ast.File, mapper *protocol.ColumnMapper, parseError error, err error) {
+func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode source.ParseMode) *parseGoData {
ctx, done := trace.StartSpan(ctx, "cache.parseGo", telemetry.File.Of(fh.Identity().URI.Filename()))
defer done()
if fh.Identity().Kind != source.Go {
- return nil, nil, nil, errors.Errorf("cannot parse non-Go file %s", fh.Identity().URI)
+ return &parseGoData{err: errors.Errorf("cannot parse non-Go file %s", fh.Identity().URI)}
}
buf, _, err := fh.Read(ctx)
if err != nil {
- return nil, nil, nil, err
+ return &parseGoData{err: err}
}
parseLimit <- struct{}{}
defer func() { <-parseLimit }()
@@ -127,21 +125,36 @@
if mode == source.ParseHeader {
parserMode = parser.ImportsOnly | parser.ParseComments
}
- file, parseError = parser.ParseFile(fset, fh.Identity().URI.Filename(), buf, parserMode)
+ file, parseError := parser.ParseFile(fset, fh.Identity().URI.Filename(), buf, parserMode)
var tok *token.File
if file != nil {
- // Fix any badly parsed parts of the AST.
tok = fset.File(file.Pos())
if tok == nil {
- return nil, nil, nil, errors.Errorf("successfully parsed but no token.File for %s (%v)", fh.Identity().URI, parseError)
+ return &parseGoData{err: errors.Errorf("successfully parsed but no token.File for %s (%v)", fh.Identity().URI, parseError)}
}
+
+ // Fix any badly parsed parts of the AST.
+ _ = fixAST(ctx, file, tok, buf)
+
+ // Fix certain syntax errors that render the file unparseable.
+ newSrc := fixSrc(file, tok, buf)
+ if newSrc != nil {
+ newFile, _ := parser.ParseFile(fset, fh.Identity().URI.Filename(), newSrc, parserMode)
+ if newFile != nil {
+ // Maintain the original parseError so we don't try formatting the doctored file.
+ file = newFile
+ buf = newSrc
+ tok = fset.File(file.Pos())
+
+ _ = fixAST(ctx, file, tok, buf)
+ }
+ }
+
if mode == source.ParseExported {
trimAST(file)
}
- if err := fix(ctx, file, tok, buf); err != nil {
- log.Error(ctx, "failed to fix AST", err)
- }
}
+
if file == nil {
// If the file is nil only due to parse errors,
// the parse errors are the actual errors.
@@ -149,19 +162,20 @@
if err == nil {
err = errors.Errorf("no AST for %s", fh.Identity().URI)
}
- return nil, nil, parseError, err
- }
- uri := fh.Identity().URI
- content, _, err := fh.Read(ctx)
- if err != nil {
- return nil, nil, parseError, err
+ return &parseGoData{parseError: parseError, err: err}
}
m := &protocol.ColumnMapper{
- URI: uri,
+ URI: fh.Identity().URI,
Converter: span.NewTokenConverter(fset, tok),
- Content: content,
+ Content: buf,
}
- return file, m, parseError, nil
+
+ return &parseGoData{
+ src: buf,
+ ast: file,
+ mapper: m,
+ parseError: parseError,
+ }
}
// trimAST clears any part of the AST not relevant to type checking
@@ -201,13 +215,59 @@
return ok
}
-// fix inspects the AST and potentially modifies any *ast.BadStmts so that it can be
+// fixAST inspects the AST and potentially modifies any *ast.BadStmts so that it can be
// type-checked more effectively.
-func fix(ctx context.Context, n ast.Node, tok *token.File, src []byte) error {
- var (
- ancestors []ast.Node
- err error
- )
+func fixAST(ctx context.Context, n ast.Node, tok *token.File, src []byte) error {
+ var err error
+ walkASTWithParent(n, func(n, parent ast.Node) bool {
+ switch n := n.(type) {
+ case *ast.BadStmt:
+ err = fixDeferOrGoStmt(n, parent, tok, src) // don't shadow err
+ if err == nil {
+ // Recursively fix in our fixed node.
+ err = fixAST(ctx, parent, tok, src)
+ } else {
+ err = errors.Errorf("unable to parse defer or go from *ast.BadStmt: %v", err)
+ }
+ return false
+ case *ast.BadExpr:
+ // Don't propagate this error since *ast.BadExpr is very common
+ // and it is only sometimes due to array types. Errors from here
+ // are expected and not actionable in general.
+ if fixArrayType(n, parent, tok, src) == nil {
+ // Recursively fix in our fixed node.
+ err = fixAST(ctx, parent, tok, src)
+ return false
+ }
+
+ // Fix cases where parser interprets if/for/switch "init"
+ // statement as "cond" expression, e.g.:
+ //
+ // // "i := foo" is init statement, not condition.
+ // for i := foo
+ //
+ fixInitStmt(n, parent, tok, src)
+
+ return false
+ case *ast.SelectorExpr:
+ // Fix cases where a keyword prefix results in a phantom "_" selector, e.g.:
+ //
+ // foo.var<> // want to complete to "foo.variance"
+ //
+ fixPhantomSelector(n, tok, src)
+ return true
+ default:
+ return true
+ }
+ })
+
+ return err
+}
+
+// walkASTWithParent walks the AST rooted at n. The semantics are
+// similar to ast.Inspect except it does not call f(nil).
+func walkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) {
+ var ancestors []ast.Node
ast.Inspect(n, func(n ast.Node) (recurse bool) {
defer func() {
if recurse {
@@ -225,90 +285,151 @@
parent = ancestors[len(ancestors)-1]
}
- switch n := n.(type) {
- case *ast.BadStmt:
- err = fixDeferOrGoStmt(n, parent, tok, src) // don't shadow err
- if err == nil {
- // Recursively fix in our fixed node.
- err = fix(ctx, parent, tok, src)
- } else {
- err = errors.Errorf("unable to parse defer or go from *ast.BadStmt: %v", err)
- }
- return false
- case *ast.BadExpr:
- // Don't propagate this error since *ast.BadExpr is very common
- // and it is only sometimes due to array types. Errors from here
- // are expected and not actionable in general.
- if fixArrayType(n, parent, tok, src) == nil {
- // Recursively fix in our fixed node.
- err = fix(ctx, parent, tok, src)
- return false
- }
-
- // Fix cases where the parser expects an expression but finds a keyword, e.g.:
- //
- // someFunc(var<>) // want to complete to "variance"
- //
- fixAccidentalKeyword(n, parent, tok, src)
-
- return false
- case *ast.DeclStmt:
- // Fix cases where the completion prefix looks like a decl, e.g.:
- //
- // func typeName(obj interface{}) string {}
- // type<> // want to call "typeName()" but looks like a "type" decl
- //
- fixAccidentalDecl(n, parent, tok, src)
- return false
- case *ast.SelectorExpr:
- // Fix cases where a keyword prefix results in a phantom "_" selector, e.g.:
- //
- // foo.var<> // want to complete to "foo.variance"
- //
- fixPhantomSelector(n, tok, src)
- return true
- default:
- return true
- }
+ return f(n, parent)
})
-
- return err
}
-// fixAccidentalDecl tries to fix "accidental" declarations. For example:
-//
-// func typeOf() {}
-// type<> // want to call typeOf(), not declare a type
-//
-// If we find an *ast.DeclStmt with only a single phantom "_" spec, we
-// replace the decl statement with an expression statement containing
-// only the keyword. This allows completion to work to some degree.
-func fixAccidentalDecl(decl *ast.DeclStmt, parent ast.Node, tok *token.File, src []byte) {
- genDecl, _ := decl.Decl.(*ast.GenDecl)
- if genDecl == nil || len(genDecl.Specs) != 1 {
- return
- }
-
- switch spec := genDecl.Specs[0].(type) {
- case *ast.TypeSpec:
- // If the name isn't a phantom "_" identifier inserted by the
- // parser then the decl is likely legitimate and we shouldn't mess
- // with it.
- if !isPhantomUnderscore(spec.Name, tok, src) {
- return
+// fixSrc attempts to modify the file's source code to fix certain
+// syntax errors that leave the rest of the file unparsed.
+func fixSrc(f *ast.File, tok *token.File, src []byte) (newSrc []byte) {
+ walkASTWithParent(f, func(n, parent ast.Node) bool {
+ if newSrc != nil {
+ return false
}
- case *ast.ValueSpec:
- if len(spec.Names) != 1 || !isPhantomUnderscore(spec.Names[0], tok, src) {
- return
- }
- }
- replaceNode(parent, decl, &ast.ExprStmt{
- X: &ast.Ident{
- Name: genDecl.Tok.String(),
- NamePos: decl.Pos(),
- },
+ switch n := n.(type) {
+ case *ast.BlockStmt:
+ newSrc = fixMissingCurlies(f, n, parent, tok, src)
+ case *ast.SelectorExpr:
+ newSrc = fixDanglingSelector(f, n, parent, tok, src)
+ }
+
+ return newSrc == nil
})
+
+ return newSrc
+}
+
+// fixMissingCurlies adds in curly braces for block statements that
+// are missing curly braces. For example:
+//
+// if foo
+//
+// becomes
+//
+// if foo {}
+func fixMissingCurlies(f *ast.File, b *ast.BlockStmt, parent ast.Node, tok *token.File, src []byte) []byte {
+ // If the "{" is already in the source code, there isn't anything to
+ // fix since we aren't mising curlies.
+ if b.Lbrace.IsValid() {
+ braceOffset := tok.Offset(b.Lbrace)
+ if braceOffset < len(src) && src[braceOffset] == '{' {
+ return nil
+ }
+ }
+
+ parentLine := tok.Line(parent.Pos())
+
+ if parentLine >= tok.LineCount() {
+ // If we are the last line in the file, no need to fix anything.
+ return nil
+ }
+
+ // Insert curlies at the end of parent's starting line. The parent
+ // is the statement that contains the block, e.g. *ast.IfStmt. The
+ // block's Pos()/End() can't be relied upon because they are based
+ // on the (missing) curly braces. We assume the statement is a
+ // single line for now and try sticking the curly braces at the end.
+ insertPos := tok.LineStart(parentLine+1) - 1
+
+ // Scootch position backwards until it's not in a comment. For example:
+ //
+ // if foo<> // some amazing comment |
+ // someOtherCode()
+ //
+ // insertPos will be located at "|", so we back it out of the comment.
+ didSomething := true
+ for didSomething {
+ didSomething = false
+ for _, c := range f.Comments {
+ if c.Pos() < insertPos && insertPos <= c.End() {
+ insertPos = c.Pos()
+ didSomething = true
+ }
+ }
+ }
+
+ // Bail out if line doesn't end in an ident or ".". This is to avoid
+ // cases like below where we end up making things worse by adding
+ // curlies:
+ //
+ // if foo &&
+ // bar<>
+ switch precedingToken(insertPos, tok, src) {
+ case token.IDENT, token.PERIOD:
+ // ok
+ default:
+ return nil
+ }
+
+ var buf bytes.Buffer
+ buf.Grow(len(src) + 3)
+ buf.Write(src[:tok.Offset(insertPos)])
+
+ // Detect if we need to insert a semicolon to fix "for" loop situations like:
+ //
+ // for i := foo(); foo<>
+ //
+ // Just adding curlies is not sufficient to make things parse well.
+ if fs, ok := parent.(*ast.ForStmt); ok {
+ if _, ok := fs.Cond.(*ast.BadExpr); !ok {
+ if xs, ok := fs.Post.(*ast.ExprStmt); ok {
+ if _, ok := xs.X.(*ast.BadExpr); ok {
+ buf.WriteByte(';')
+ }
+ }
+ }
+ }
+
+ // Insert "{}" at insertPos.
+ buf.WriteByte('{')
+ buf.WriteByte('}')
+ buf.Write(src[tok.Offset(insertPos):])
+ return buf.Bytes()
+}
+
+// fixDanglingSelector inserts real "_" selector expressions in place
+// of phantom "_" selectors. For example:
+//
+// func _() {
+// x.<>
+// }
+// var x struct { i int }
+//
+// To fix completion at "<>", we insert a real "_" after the "." so the
+// following declaration of "x" can be parsed and type checked
+// normally.
+func fixDanglingSelector(f *ast.File, s *ast.SelectorExpr, parent ast.Node, tok *token.File, src []byte) []byte {
+ if !isPhantomUnderscore(s.Sel, tok, src) {
+ return nil
+ }
+
+ if !s.X.End().IsValid() {
+ return nil
+ }
+
+ // Insert directly after the selector's ".".
+ insertOffset := tok.Offset(s.X.End()) + 1
+ if src[insertOffset-1] != '.' {
+ return nil
+ }
+
+ var buf bytes.Buffer
+ buf.Grow(len(src) + 1)
+ buf.Write(src[:insertOffset])
+ buf.WriteByte('_')
+ buf.Write(src[insertOffset:])
+ return buf.Bytes()
}
// fixPhantomSelector tries to fix selector expressions with phantom
@@ -323,6 +444,16 @@
return
}
+ // Only consider selectors directly abutting the selector ".". This
+ // avoids false positives in cases like:
+ //
+ // foo. // don't think "var" is our selector
+ // var bar = 123
+ //
+ if sel.Sel.Pos() != sel.X.End()+1 {
+ return
+ }
+
maybeKeyword := readKeyword(sel.Sel.Pos(), tok, src)
if maybeKeyword == "" {
return
@@ -348,25 +479,46 @@
return len(src) <= offset || src[offset] != '_'
}
-// fixAccidentalKeyword tries to fix "accidental" keyword expressions. For example:
-//
-// variance := 123
-// doMath(var<>)
-//
-// If we find an *ast.BadExpr that begins with a keyword, we replace
-// the BadExpr with an *ast.Ident containing the text of the keyword.
-// This allows completion to work to some degree.
-func fixAccidentalKeyword(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) {
- if !bad.Pos().IsValid() {
+// fixInitStmt fixes cases where the parser misinterprets an
+// if/for/switch "init" statement as the "cond" conditional. In cases
+// like "if i := 0" the user hasn't typed the semicolon yet so the
+// parser is looking for the conditional expression. However, "i := 0"
+// are not valid expressions, so we get a BadExpr.
+func fixInitStmt(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) {
+ if !bad.Pos().IsValid() || !bad.End().IsValid() {
return
}
- maybeKeyword := readKeyword(bad.Pos(), tok, src)
- if maybeKeyword == "" {
+ // Try to extract a statement from the BadExpr.
+ stmtBytes := src[tok.Offset(bad.Pos()) : tok.Offset(bad.End()-1)+1]
+ stmt, err := parseStmt(bad.Pos(), stmtBytes)
+ if err != nil {
return
}
- replaceNode(parent, bad, &ast.Ident{Name: maybeKeyword, NamePos: bad.Pos()})
+ // If the parent statement doesn't already have an "init" statement,
+ // move the extracted statement into the "init" field and insert a
+ // dummy expression into the required "cond" field.
+ switch p := parent.(type) {
+ case *ast.IfStmt:
+ if p.Init != nil {
+ return
+ }
+ p.Init = stmt
+ p.Cond = &ast.Ident{Name: "_"}
+ case *ast.ForStmt:
+ if p.Init != nil {
+ return
+ }
+ p.Init = stmt
+ p.Cond = &ast.Ident{Name: "_"}
+ case *ast.SwitchStmt:
+ if p.Init != nil {
+ return
+ }
+ p.Init = stmt
+ p.Tag = nil
+ }
}
// readKeyword reads the keyword starting at pos, if any.
@@ -443,6 +595,23 @@
return nil
}
+// precedingToken scans src to find the token preceding pos.
+func precedingToken(pos token.Pos, tok *token.File, src []byte) token.Token {
+ s := &scanner.Scanner{}
+ s.Init(tok, src, nil, 0)
+
+ var lastTok token.Token
+ for {
+ p, t, _ := s.Scan()
+ if t == token.EOF || p >= pos {
+ break
+ }
+
+ lastTok = t
+ }
+ return lastTok
+}
+
// fixDeferOrGoStmt tries to parse an *ast.BadStmt into a defer or a go statement.
//
// go/parser packages a statement of the form "defer x." as an *ast.BadStmt because
@@ -597,9 +766,9 @@
return nil
}
-// parseExpr parses the expression in src and updates its position to
+// parseStmt parses the statement in src and updates its position to
// start at pos.
-func parseExpr(pos token.Pos, src []byte) (ast.Expr, error) {
+func parseStmt(pos token.Pos, src []byte) (ast.Stmt, error) {
// Wrap our expression to make it a valid Go file we can pass to ParseFile.
fileSrc := bytes.Join([][]byte{
[]byte("package fake;func _(){"),
@@ -624,25 +793,36 @@
return nil, errors.Errorf("no statement in %s: %v", src, err)
}
- exprStmt, ok := fakeDecl.Body.List[0].(*ast.ExprStmt)
+ stmt := fakeDecl.Body.List[0]
+
+ // parser.ParseFile returns undefined positions.
+ // Adjust them for the current file.
+ offsetPositions(stmt, pos-1-(stmt.Pos()-1))
+
+ return stmt, nil
+}
+
+// parseExpr parses the expression in src and updates its position to
+// start at pos.
+func parseExpr(pos token.Pos, src []byte) (ast.Expr, error) {
+ stmt, err := parseStmt(pos, src)
+ if err != nil {
+ return nil, err
+ }
+
+ exprStmt, ok := stmt.(*ast.ExprStmt)
if !ok {
return nil, errors.Errorf("no expr in %s: %v", src, err)
}
- expr := exprStmt.X
-
- // parser.ParseExpr returns undefined positions.
- // Adjust them for the current file.
- offsetPositions(expr, pos-1-(expr.Pos()-1))
-
- return expr, nil
+ return exprStmt.X, nil
}
var tokenPosType = reflect.TypeOf(token.NoPos)
// offsetPositions applies an offset to the positions in an ast.Node.
-func offsetPositions(expr ast.Expr, offset token.Pos) {
- ast.Inspect(expr, func(n ast.Node) bool {
+func offsetPositions(n ast.Node, offset token.Pos) {
+ ast.Inspect(n, func(n ast.Node) bool {
if n == nil {
return false
}
@@ -670,7 +850,7 @@
}
// replaceNode updates parent's child oldChild to be newChild. It
-// retuns whether it replaced successfully.
+// returns whether it replaced successfully.
func replaceNode(parent, oldChild, newChild ast.Node) bool {
if parent == nil || oldChild == nil || newChild == nil {
return false
diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go
index f7ae413..db32834 100644
--- a/internal/lsp/cache/pkg.go
+++ b/internal/lsp/cache/pkg.go
@@ -11,6 +11,7 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
)
@@ -18,14 +19,15 @@
// pkg contains the type information needed by the source package.
type pkg struct {
// ID and package path have their own types to avoid being used interchangeably.
- id packageID
- pkgPath packagePath
- mode source.ParseMode
-
+ id packageID
+ pkgPath packagePath
+ mode source.ParseMode
+ forTest packagePath
goFiles []source.ParseGoHandle
compiledGoFiles []source.ParseGoHandle
errors []*source.Error
imports map[packagePath]*pkg
+ module *packagesinternal.Module
types *types.Package
typesInfo *types.Info
typesSizes types.Sizes
@@ -71,7 +73,7 @@
func (p *pkg) GetSyntax() []*ast.File {
var syntax []*ast.File
for _, ph := range p.compiledGoFiles {
- file, _, _, err := ph.Cached()
+ file, _, _, _, err := ph.Cached()
if err == nil {
syntax = append(syntax, file)
}
@@ -99,6 +101,10 @@
return p.types == nil || p.typesInfo == nil || p.typesSizes == nil
}
+func (p *pkg) ForTest() string {
+ return string(p.forTest)
+}
+
func (p *pkg) GetImport(pkgPath string) (source.Package, error) {
if imp := p.imports[packagePath(pkgPath)]; imp != nil {
return imp, nil
@@ -115,12 +121,16 @@
return result
}
+func (p *pkg) Module() *packagesinternal.Module {
+ return p.module
+}
+
func (s *snapshot) FindAnalysisError(ctx context.Context, pkgID, analyzerName, msg string, rng protocol.Range) (*source.Error, error) {
analyzer, ok := s.View().Options().Analyzers[analyzerName]
if !ok {
return nil, errors.Errorf("unexpected analyzer: %s", analyzerName)
}
- act, err := s.actionHandle(ctx, packageID(pkgID), source.ParseFull, analyzer)
+ act, err := s.actionHandle(ctx, packageID(pkgID), analyzer)
if err != nil {
return nil, err
}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index d8de7d2..eceb84d 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -11,7 +11,6 @@
"sync"
"sync/atomic"
- "golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/trace"
@@ -19,8 +18,8 @@
errors "golang.org/x/xerrors"
)
-type session struct {
- cache *cache
+type Session struct {
+ cache *Cache
id string
options source.Options
@@ -33,15 +32,44 @@
overlays map[span.URI]*overlay
}
-func (s *session) Options() source.Options {
+type overlay struct {
+ session *Session
+ uri span.URI
+ text []byte
+ hash string
+ version float64
+ kind source.FileKind
+
+ // saved is true if a file has been saved on disk,
+ // and therefore does not need to be part of the overlay sent to go/packages.
+ saved bool
+}
+
+func (o *overlay) FileSystem() source.FileSystem {
+ return o.session
+}
+
+func (o *overlay) Identity() source.FileIdentity {
+ return source.FileIdentity{
+ URI: o.uri,
+ Identifier: o.hash,
+ Version: o.version,
+ Kind: o.kind,
+ }
+}
+func (o *overlay) Read(ctx context.Context) ([]byte, string, error) {
+ return o.text, o.hash, nil
+}
+
+func (s *Session) Options() source.Options {
return s.options
}
-func (s *session) SetOptions(options source.Options) {
+func (s *Session) SetOptions(options source.Options) {
s.options = options
}
-func (s *session) Shutdown(ctx context.Context) {
+func (s *Session) Shutdown(ctx context.Context) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, view := range s.views {
@@ -49,14 +77,14 @@
}
s.views = nil
s.viewMap = nil
- debug.DropSession(debugSession{s})
+ s.cache.debug.DropSession(DebugSession{s})
}
-func (s *session) Cache() source.Cache {
+func (s *Session) Cache() source.Cache {
return s.cache
}
-func (s *session) NewView(ctx context.Context, name string, folder span.URI, options source.Options) (source.View, source.Snapshot, error) {
+func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options source.Options) (source.View, source.Snapshot, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
v, snapshot, err := s.createView(ctx, name, folder, options)
@@ -69,7 +97,7 @@
return v, snapshot, nil
}
-func (s *session) createView(ctx context.Context, name string, folder span.URI, options source.Options) (*view, *snapshot, error) {
+func (s *Session) createView(ctx context.Context, name string, folder span.URI, options source.Options) (*view, *snapshot, error) {
index := atomic.AddInt64(&viewIndex, 1)
// We want a true background context and not a detached context here
// the spans need to be unrelated and no tag values should pollute it.
@@ -97,6 +125,7 @@
actions: make(map[actionKey]*actionHandle),
workspacePackages: make(map[packageID]packagePath),
unloadableFiles: make(map[span.URI]struct{}),
+ modHandles: make(map[span.URI]*modHandle),
},
ignoredURIs: make(map[span.URI]struct{}),
}
@@ -113,12 +142,12 @@
// Initialize the view without blocking.
go v.initialize(xcontext.Detach(ctx), v.snapshot)
- debug.AddView(debugView{v})
+ v.session.cache.debug.AddView(debugView{v})
return v, v.snapshot, nil
}
// View returns the view by name.
-func (s *session) View(name string) source.View {
+func (s *Session) View(name string) source.View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, view := range s.views {
@@ -131,11 +160,11 @@
// ViewOf returns a view corresponding to the given URI.
// If the file is not already associated with a view, pick one using some heuristics.
-func (s *session) ViewOf(uri span.URI) (source.View, error) {
+func (s *Session) ViewOf(uri span.URI) (source.View, error) {
return s.viewOf(uri)
}
-func (s *session) viewOf(uri span.URI) (*view, error) {
+func (s *Session) viewOf(uri span.URI) (*view, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
@@ -152,7 +181,7 @@
return v, nil
}
-func (s *session) viewsOf(uri span.URI) []*view {
+func (s *Session) viewsOf(uri span.URI) []*view {
s.viewMu.Lock()
defer s.viewMu.Unlock()
@@ -165,7 +194,7 @@
return views
}
-func (s *session) Views() []source.View {
+func (s *Session) Views() []source.View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
result := make([]source.View, len(s.views))
@@ -177,7 +206,7 @@
// bestView finds the best view to associate a given URI with.
// viewMu must be held when calling this method.
-func (s *session) bestView(uri span.URI) (*view, error) {
+func (s *Session) bestView(uri span.URI) (*view, error) {
if len(s.views) == 0 {
return nil, errors.Errorf("no views in the session")
}
@@ -187,7 +216,7 @@
if longest != nil && len(longest.Folder()) > len(view.Folder()) {
continue
}
- if strings.HasPrefix(string(uri), string(view.Folder())) {
+ if view.contains(uri) {
longest = view
}
}
@@ -198,7 +227,7 @@
return s.views[0], nil
}
-func (s *session) removeView(ctx context.Context, view *view) error {
+func (s *Session) removeView(ctx context.Context, view *view) error {
s.viewMu.Lock()
defer s.viewMu.Unlock()
i, err := s.dropView(ctx, view)
@@ -213,7 +242,7 @@
return nil
}
-func (s *session) updateView(ctx context.Context, view *view, options source.Options) (*view, *snapshot, error) {
+func (s *Session) updateView(ctx context.Context, view *view, options source.Options) (*view, *snapshot, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
i, err := s.dropView(ctx, view)
@@ -234,7 +263,7 @@
return v, snapshot, nil
}
-func (s *session) dropView(ctx context.Context, v *view) (int, error) {
+func (s *Session) dropView(ctx context.Context, v *view) (int, error) {
// we always need to drop the view map
s.viewMap = make(map[span.URI]*view)
for i := range s.views {
@@ -248,15 +277,18 @@
return -1, errors.Errorf("view %s for %v not found", v.Name(), v.Folder())
}
-func (s *session) DidModifyFiles(ctx context.Context, changes []source.FileModification) ([]source.Snapshot, error) {
- views := make(map[*view][]span.URI)
+func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) ([]source.Snapshot, error) {
+ views := make(map[*view]map[span.URI]source.FileHandle)
+ overlays, err := s.updateOverlays(ctx, changes)
+ if err != nil {
+ return nil, err
+ }
for _, c := range changes {
- // Only update overlays for in-editor changes.
- if !c.OnDisk {
- if err := s.updateOverlay(ctx, c); err != nil {
- return nil, err
- }
+ // Do nothing if the file is open in the editor and we receive
+ // an on-disk action. The editor is the source of truth.
+ if s.isOpen(c.URI) && c.OnDisk {
+ continue
}
// Look through all of the session's views, invalidating the file for
// all of the views to which it is known.
@@ -273,7 +305,14 @@
if _, err := view.getFile(c.URI); err != nil {
return nil, err
}
- views[view] = append(views[view], c.URI)
+ if _, ok := views[view]; !ok {
+ views[view] = make(map[span.URI]source.FileHandle)
+ }
+ if o, ok := overlays[c.URI]; ok {
+ views[view][c.URI] = o
+ } else {
+ views[view][c.URI] = s.cache.GetFile(c.URI)
+ }
}
}
var snapshots []source.Snapshot
@@ -283,7 +322,7 @@
return snapshots, nil
}
-func (s *session) IsOpen(uri span.URI) bool {
+func (s *Session) isOpen(uri span.URI) bool {
s.overlayMu.Lock()
defer s.overlayMu.Unlock()
@@ -291,10 +330,96 @@
return open
}
-func (s *session) GetFile(uri span.URI) source.FileHandle {
+func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModification) (map[span.URI]*overlay, error) {
+ s.overlayMu.Lock()
+ defer s.overlayMu.Unlock()
+
+ for _, c := range changes {
+ // Don't update overlays for on-disk changes.
+ if c.OnDisk {
+ continue
+ }
+
+ o, ok := s.overlays[c.URI]
+
+ // Determine the file kind on open, otherwise, assume it has been cached.
+ var kind source.FileKind
+ switch c.Action {
+ case source.Open:
+ kind = source.DetectLanguage(c.LanguageID, c.URI.Filename())
+ default:
+ if !ok {
+ return nil, errors.Errorf("updateOverlays: modifying unopened overlay %v", c.URI)
+ }
+ kind = o.kind
+ }
+ if kind == source.UnknownKind {
+ return nil, errors.Errorf("updateOverlays: unknown file kind for %s", c.URI)
+ }
+
+ // Closing a file just deletes its overlay.
+ if c.Action == source.Close {
+ delete(s.overlays, c.URI)
+ continue
+ }
+
+ // If the file is on disk, check if its content is the same as the overlay.
+ text := c.Text
+ if text == nil {
+ text = o.text
+ }
+ hash := hashContents(text)
+ var sameContentOnDisk bool
+ switch c.Action {
+ case source.Open:
+ _, h, err := s.cache.GetFile(c.URI).Read(ctx)
+ sameContentOnDisk = (err == nil && h == hash)
+ case source.Save:
+ // Make sure the version and content (if present) is the same.
+ if o.version != c.Version {
+ return nil, errors.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version)
+ }
+ if c.Text != nil && o.hash != hash {
+ return nil, errors.Errorf("updateOverlays: overlay %s changed on save", c.URI)
+ }
+ sameContentOnDisk = true
+ }
+ o = &overlay{
+ session: s,
+ uri: c.URI,
+ version: c.Version,
+ text: text,
+ kind: kind,
+ hash: hash,
+ saved: sameContentOnDisk,
+ }
+ s.overlays[c.URI] = o
+ }
+
+ // Get the overlays for each change while the session's overlay map is locked.
+ overlays := make(map[span.URI]*overlay)
+ for _, c := range changes {
+ if o, ok := s.overlays[c.URI]; ok {
+ overlays[c.URI] = o
+ }
+ }
+ return overlays, nil
+}
+
+func (s *Session) GetFile(uri span.URI) source.FileHandle {
if overlay := s.readOverlay(uri); overlay != nil {
return overlay
}
// Fall back to the cache-level file system.
return s.cache.GetFile(uri)
}
+
+func (s *Session) readOverlay(uri span.URI) *overlay {
+ s.overlayMu.Lock()
+ defer s.overlayMu.Unlock()
+
+ if overlay, ok := s.overlays[uri]; ok {
+ return overlay
+ }
+ return nil
+}
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index ce3641f..4391584 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -6,14 +6,19 @@
import (
"context"
+ "fmt"
+ "go/ast"
+ "go/token"
"os"
"path/filepath"
"sync"
"golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
"golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/telemetry/log"
errors "golang.org/x/xerrors"
)
@@ -52,6 +57,9 @@
// unloadableFiles keeps track of files that we've failed to load.
unloadableFiles map[span.URI]struct{}
+
+ // modHandles keeps track of any ParseModHandles for this snapshot.
+ modHandles map[span.URI]*modHandle
}
type packageKey struct {
@@ -64,145 +72,119 @@
analyzer *analysis.Analyzer
}
+func (s *snapshot) ID() uint64 {
+ return s.id
+}
+
func (s *snapshot) View() source.View {
return s.view
}
+// Config returns the configuration used for the snapshot's interaction with the
+// go/packages API.
+func (s *snapshot) Config(ctx context.Context) *packages.Config {
+ env, buildFlags := s.view.env()
+ cfg := &packages.Config{
+ Env: env,
+ Dir: s.view.folder.Filename(),
+ Context: ctx,
+ BuildFlags: buildFlags,
+ Mode: packages.NeedName |
+ packages.NeedFiles |
+ packages.NeedCompiledGoFiles |
+ packages.NeedImports |
+ packages.NeedDeps |
+ packages.NeedTypesSizes,
+ Fset: s.view.session.cache.fset,
+ Overlay: s.buildOverlay(),
+ ParseFile: func(*token.FileSet, string, []byte) (*ast.File, error) {
+ panic("go/packages must not be used to parse files")
+ },
+ Logf: func(format string, args ...interface{}) {
+ if s.view.options.VerboseOutput {
+ log.Print(ctx, fmt.Sprintf(format, args...))
+ }
+ },
+ Tests: true,
+ }
+ return cfg
+}
+
+func (s *snapshot) buildOverlay() map[string][]byte {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ overlays := make(map[string][]byte)
+ for uri, fh := range s.files {
+ overlay, ok := fh.(*overlay)
+ if !ok {
+ continue
+ }
+ if overlay.saved {
+ continue
+ }
+ // TODO(rstambler): Make sure not to send overlays outside of the current view.
+ overlays[uri.Filename()] = overlay.text
+ }
+ return overlays
+}
+
func (s *snapshot) PackageHandles(ctx context.Context, fh source.FileHandle) ([]source.PackageHandle, error) {
- // If the file is a go.mod file, go.Packages.Load will always return 0 packages.
- if fh.Identity().Kind == source.Mod {
- return nil, errors.Errorf("attempting to get PackageHandles of .mod file %s", fh.Identity().URI)
+ if fh.Identity().Kind != source.Go {
+ panic("called PackageHandles on a non-Go FileHandle")
}
ctx = telemetry.File.With(ctx, fh.Identity().URI)
- meta := s.getMetadataForURI(fh.Identity().URI)
- phs, err := s.packageHandles(ctx, fileURI(fh.Identity().URI), meta)
- if err != nil {
- return nil, err
+ // Check if we should reload metadata for the file. We don't invalidate IDs
+ // (though we should), so the IDs will be a better source of truth than the
+ // metadata. If there are no IDs for the file, then we should also reload.
+ ids := s.getIDsForURI(fh.Identity().URI)
+ reload := len(ids) == 0
+ for _, id := range ids {
+ // Reload package metadata if any of the metadata has missing
+ // dependencies, in case something has changed since the last time we
+ // reloaded it.
+ if m := s.getMetadata(id); m == nil {
+ reload = true
+ break
+ }
+ // TODO(golang/go#36918): Previously, we would reload any package with
+ // missing dependencies. This is expensive and results in too many
+ // calls to packages.Load. Determine what we should do instead.
}
- var results []source.PackageHandle
- for _, ph := range phs {
- results = append(results, ph)
- }
- return results, nil
-}
-
-func (s *snapshot) packageHandle(ctx context.Context, id packageID) (*packageHandle, error) {
- m := s.getMetadata(id)
-
- // Don't reload metadata in this function.
- // Callers of this function must reload metadata themselves.
- if m == nil {
- return nil, errors.Errorf("%s has no metadata", id)
- }
- phs, load, check := s.shouldCheck([]*metadata{m})
- if load {
- return nil, errors.Errorf("%s needs loading", id)
- }
- if check {
- return s.buildPackageHandle(ctx, m.id, source.ParseFull)
- }
- var result *packageHandle
- for _, ph := range phs {
- if ph.m.id == id {
- if result != nil {
- return nil, errors.Errorf("multiple package handles for the same ID: %s", id)
- }
- result = ph
+ if reload {
+ if err := s.load(ctx, fileURI(fh.Identity().URI)); err != nil {
+ return nil, err
}
}
- if result == nil {
- return nil, errors.Errorf("no PackageHandle for %s", id)
- }
- return result, nil
-}
-
-func (s *snapshot) packageHandles(ctx context.Context, scope interface{}, meta []*metadata) ([]*packageHandle, error) {
- // First, determine if we need to reload or recheck the package.
- phs, load, check := s.shouldCheck(meta)
- if load {
- newMeta, err := s.load(ctx, scope)
+ // Get the list of IDs from the snapshot again, in case it has changed.
+ var phs []source.PackageHandle
+ for _, id := range s.getIDsForURI(fh.Identity().URI) {
+ ph, err := s.packageHandle(ctx, id, source.ParseFull)
if err != nil {
return nil, err
}
- newMissing := missingImports(newMeta)
- if len(newMissing) != 0 {
- // Type checking a package with the same missing imports over and over
- // is futile. Don't re-check unless something has changed.
- check = check && !sameSet(missingImports(meta), newMissing)
- }
- meta = newMeta
- }
- var results []*packageHandle
- if check {
- for _, m := range meta {
- ph, err := s.buildPackageHandle(ctx, m.id, source.ParseFull)
- if err != nil {
- return nil, err
- }
- results = append(results, ph)
- }
- } else {
- results = phs
- }
- if len(results) == 0 {
- return nil, errors.Errorf("packageHandles: no package handles for %v", scope)
- }
- return results, nil
-}
-
-func missingImports(metadata []*metadata) map[packagePath]struct{} {
- result := map[packagePath]struct{}{}
- for _, m := range metadata {
- for path := range m.missingDeps {
- result[path] = struct{}{}
- }
- }
- return result
-}
-
-func sameSet(x, y map[packagePath]struct{}) bool {
- if len(x) != len(y) {
- return false
- }
- for k := range x {
- if _, ok := y[k]; !ok {
- return false
- }
- }
- return true
-}
-
-// shouldCheck determines if the packages provided by the metadata
-// need to be re-loaded or re-type-checked.
-func (s *snapshot) shouldCheck(m []*metadata) (phs []*packageHandle, load, check bool) {
- // No metadata. Re-load and re-check.
- if len(m) == 0 {
- return nil, true, true
- }
- // We expect to see a checked package for each package ID,
- // and it should be parsed in full mode.
- // If a single PackageHandle is missing, re-check all of them.
- // TODO: Optimize this by only checking the necessary packages.
- for _, metadata := range m {
- ph := s.getPackage(metadata.id, source.ParseFull)
- if ph == nil {
- return nil, false, true
- }
phs = append(phs, ph)
}
- // If the metadata for the package had missing dependencies,
- // we _may_ need to re-check. If the missing dependencies haven't changed
- // since previous load, we will not check again.
- if len(phs) < len(m) {
- for _, m := range m {
- if len(m.missingDeps) != 0 {
- return nil, true, true
- }
- }
+ return phs, nil
+}
+
+// packageHandle returns a PackageHandle for the given ID. It assumes that
+// the metadata for the given ID has already been loaded, but if the
+// PackageHandle has not been constructed, it will rebuild it.
+func (s *snapshot) packageHandle(ctx context.Context, id packageID, mode source.ParseMode) (*packageHandle, error) {
+ ph := s.getPackage(id, mode)
+ if ph != nil {
+ return ph, nil
}
- return phs, false, false
+ // Don't reload metadata in this function.
+ // Callers of this function must reload metadata themselves.
+ m := s.getMetadata(id)
+ if m == nil {
+ return nil, errors.Errorf("%s has no metadata", id)
+ }
+ return s.buildPackageHandle(ctx, m.id, mode)
}
func (s *snapshot) GetReverseDependencies(ctx context.Context, id string) ([]source.PackageHandle, error) {
@@ -217,7 +199,7 @@
var results []source.PackageHandle
for id := range ids {
- ph, err := s.packageHandle(ctx, id)
+ ph, err := s.packageHandle(ctx, id, source.ParseFull)
if err != nil {
return nil, err
}
@@ -242,6 +224,12 @@
}
}
+func (s *snapshot) getModHandle(uri span.URI) *modHandle {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.modHandles[uri]
+}
+
func (s *snapshot) getImportedBy(id packageID) []packageID {
s.mu.Lock()
defer s.mu.Unlock()
@@ -302,7 +290,7 @@
}
var results []source.PackageHandle
for _, pkgID := range s.workspacePackageIDs() {
- ph, err := s.packageHandle(ctx, pkgID)
+ ph, err := s.packageHandle(ctx, pkgID, source.ParseFull)
if err != nil {
return nil, err
}
@@ -326,7 +314,7 @@
var results []source.PackageHandle
for pkgID := range wsPackages {
- ph, err := s.packageHandle(ctx, pkgID)
+ ph, err := s.packageHandle(ctx, pkgID, source.ParseFull)
if err != nil {
return nil, err
}
@@ -384,13 +372,13 @@
return results, nil
}
-func (s *snapshot) getPackage(id packageID, m source.ParseMode) *packageHandle {
+func (s *snapshot) getPackage(id packageID, mode source.ParseMode) *packageHandle {
s.mu.Lock()
defer s.mu.Unlock()
key := packageKey{
id: id,
- mode: m,
+ mode: mode,
}
return s.packages[key]
}
@@ -426,11 +414,11 @@
s.actions[key] = ah
}
-func (s *snapshot) getMetadataForURI(uri span.URI) []*metadata {
+func (s *snapshot) getIDsForURI(uri span.URI) []packageID {
s.mu.Lock()
defer s.mu.Unlock()
- return s.getMetadataForURILocked(uri)
+ return s.ids[uri]
}
func (s *snapshot) getMetadataForURILocked(uri span.URI) (metadata []*metadata) {
@@ -445,19 +433,6 @@
return metadata
}
-func (s *snapshot) setMetadata(m *metadata) {
- s.mu.Lock()
- defer s.mu.Unlock()
-
- // TODO: We should make sure not to set duplicate metadata,
- // and instead panic here. This can be done by making sure not to
- // reset metadata information for packages we've already seen.
- if _, ok := s.metadata[m.id]; ok {
- return
- }
- s.metadata[m.id] = m
-}
-
func (s *snapshot) getMetadata(id packageID) *metadata {
s.mu.Lock()
defer s.mu.Unlock()
@@ -469,24 +444,23 @@
s.mu.Lock()
defer s.mu.Unlock()
- for _, existingID := range s.ids[uri] {
+ for i, existingID := range s.ids[uri] {
+ // TODO: We should make sure not to set duplicate IDs,
+ // and instead panic here. This can be done by making sure not to
+ // reset metadata information for packages we've already seen.
if existingID == id {
- // TODO: We should make sure not to set duplicate IDs,
- // and instead panic here. This can be done by making sure not to
- // reset metadata information for packages we've already seen.
+ return
+ }
+ // If we are setting a real ID, when the package had only previously
+ // had a command-line-arguments ID, we should just replace it.
+ if existingID == "command-line-arguments" {
+ s.ids[uri][i] = id
return
}
}
s.ids[uri] = append(s.ids[uri], id)
}
-func (s *snapshot) getIDs(uri span.URI) []packageID {
- s.mu.Lock()
- defer s.mu.Unlock()
-
- return s.ids[uri]
-}
-
func (s *snapshot) isWorkspacePackage(id packageID) (packagePath, bool) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -495,17 +469,6 @@
return scope, ok
}
-func (s *snapshot) getFileURIs() []span.URI {
- s.mu.Lock()
- defer s.mu.Unlock()
-
- var uris []span.URI
- for uri := range s.files {
- uris = append(uris, uri)
- }
- return uris
-}
-
// GetFile returns a File for the given URI. It will always succeed because it
// adds the file to the managed set if needed.
func (s *snapshot) GetFile(uri span.URI) (source.FileHandle, error) {
@@ -513,51 +476,44 @@
if err != nil {
return nil, err
}
- return s.getFileHandle(f), nil
-}
-func (s *snapshot) getFileHandle(f *fileBase) source.FileHandle {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.files[f.URI()]; !ok {
- s.files[f.URI()] = s.view.session.GetFile(f.URI())
+ s.files[f.URI()] = s.view.session.cache.GetFile(uri)
}
- return s.files[f.URI()]
+ return s.files[f.URI()], nil
}
-func (s *snapshot) findFileHandle(f *fileBase) source.FileHandle {
+func (s *snapshot) IsOpen(uri span.URI) bool {
s.mu.Lock()
defer s.mu.Unlock()
- return s.files[f.URI()]
+ _, open := s.files[uri].(*overlay)
+ return open
+}
+
+func (s *snapshot) IsSaved(uri span.URI) bool {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ ovl, open := s.files[uri].(*overlay)
+ return !open || ovl.saved
}
func (s *snapshot) awaitLoaded(ctx context.Context) error {
// Do not return results until the snapshot's view has been initialized.
s.view.awaitInitialized(ctx)
- m, err := s.reloadWorkspace(ctx)
- if err != nil {
+ if err := s.reloadWorkspace(ctx); err != nil {
return err
}
- for _, m := range m {
- s.setWorkspacePackage(ctx, m)
- }
- if err := s.reloadOrphanedFiles(ctx); err != nil {
- return err
- }
- // Create package handles for all of the workspace packages.
- for _, id := range s.workspacePackageIDs() {
- if _, err := s.packageHandle(ctx, id); err != nil {
- return err
- }
- }
- return nil
+ return s.reloadOrphanedFiles(ctx)
}
// reloadWorkspace reloads the metadata for all invalidated workspace packages.
-func (s *snapshot) reloadWorkspace(ctx context.Context) ([]*metadata, error) {
+func (s *snapshot) reloadWorkspace(ctx context.Context) error {
// If the view's build configuration is invalid, we cannot reload by package path.
// Just reload the directory instead.
if !s.view.hasValidBuildConfiguration {
@@ -575,9 +531,8 @@
s.mu.Unlock()
if len(pkgPaths) == 0 {
- return nil, nil
+ return nil
}
-
return s.load(ctx, pkgPaths...)
}
@@ -591,7 +546,7 @@
return nil
}
- m, err := s.load(ctx, scopes...)
+ err := s.load(ctx, scopes...)
// If we failed to load some files, i.e. they have no metadata,
// mark the failures so we don't bother retrying until the file's
@@ -612,9 +567,6 @@
}
s.mu.Unlock()
}
- for _, m := range m {
- s.setWorkspacePackage(ctx, m)
- }
return nil
}
@@ -657,20 +609,7 @@
return false
}
-func (s *snapshot) setWorkspacePackage(ctx context.Context, m *metadata) {
- s.mu.Lock()
- defer s.mu.Unlock()
-
- // A test variant of a package can only be loaded directly by loading
- // the non-test variant with -test. Track the import path of the non-test variant.
- pkgPath := m.pkgPath
- if m.forTest != "" {
- pkgPath = m.forTest
- }
- s.workspacePackages[m.id] = pkgPath
-}
-
-func (s *snapshot) clone(ctx context.Context, withoutURIs []span.URI) *snapshot {
+func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.FileHandle) *snapshot {
s.mu.Lock()
defer s.mu.Unlock()
@@ -685,6 +624,7 @@
files: make(map[span.URI]source.FileHandle),
workspacePackages: make(map[packageID]packagePath),
unloadableFiles: make(map[span.URI]struct{}),
+ modHandles: make(map[span.URI]*modHandle),
}
// Copy all of the FileHandles.
@@ -695,13 +635,17 @@
for k, v := range s.unloadableFiles {
result.unloadableFiles[k] = v
}
+ // Copy all of the modHandles.
+ for k, v := range s.modHandles {
+ result.modHandles[k] = v
+ }
// transitiveIDs keeps track of transitive reverse dependencies.
// If an ID is present in the map, invalidate its types.
// If an ID's value is true, invalidate its metadata too.
transitiveIDs := make(map[packageID]bool)
- for _, withoutURI := range withoutURIs {
+ for withoutURI, currentFH := range withoutURIs {
directIDs := map[packageID]struct{}{}
// Collect all of the package IDs that correspond to the given file.
@@ -709,13 +653,12 @@
for _, id := range s.ids[withoutURI] {
directIDs[id] = struct{}{}
}
- // Get the current and original FileHandles for this URI.
- currentFH := s.view.session.GetFile(withoutURI)
+ // The original FileHandle for this URI is cached on the snapshot.
originalFH := s.files[withoutURI]
// Check if the file's package name or imports have changed,
// and if so, invalidate this file's packages' metadata.
- invalidateMetadata := s.view.session.cache.shouldLoad(ctx, s, originalFH, currentFH)
+ invalidateMetadata := s.shouldInvalidateMetadata(ctx, originalFH, currentFH)
// If a go.mod file's contents have changed, invalidate the metadata
// for all of the packages in the workspace.
@@ -723,6 +666,7 @@
for id := range s.workspacePackages {
directIDs[id] = struct{}{}
}
+ delete(result.modHandles, withoutURI)
}
// If this is a file we don't yet know about,
@@ -770,10 +714,6 @@
delete(result.unloadableFiles, withoutURI)
}
- // Collect the IDs for the packages associated with the excluded URIs.
- for k, ids := range s.ids {
- result.ids[k] = ids
- }
// Copy the set of initally loaded packages.
for k, v := range s.workspacePackages {
result.workspacePackages[k] = v
@@ -800,12 +740,63 @@
}
result.metadata[k] = v
}
+ // Copy the URI to package ID mappings, skipping only those URIs whose
+ // metadata will be reloaded in future calls to load.
+outer:
+ for k, ids := range s.ids {
+ for _, id := range ids {
+ if invalidateMetadata, ok := transitiveIDs[id]; invalidateMetadata && ok {
+ continue outer
+ }
+ }
+ result.ids[k] = ids
+ }
// Don't bother copying the importedBy graph,
// as it changes each time we update metadata.
return result
}
-func (s *snapshot) ID() uint64 {
- return s.id
+// shouldInvalidateMetadata reparses a file's package and import declarations to
+// determine if the file requires a metadata reload.
+func (s *snapshot) shouldInvalidateMetadata(ctx context.Context, originalFH, currentFH source.FileHandle) bool {
+ if originalFH == nil {
+ return currentFH.Identity().Kind == source.Go
+ }
+ // If the file hasn't changed, there's no need to reload.
+ if originalFH.Identity().String() == currentFH.Identity().String() {
+ return false
+ }
+ // If a go.mod file's contents have changed, always invalidate metadata.
+ if kind := originalFH.Identity().Kind; kind == source.Mod {
+ modfile, _ := s.view.ModFiles()
+ return originalFH.Identity().URI == modfile
+ }
+ // Get the original and current parsed files in order to check package name and imports.
+ original, _, _, _, originalErr := s.view.session.cache.ParseGoHandle(originalFH, source.ParseHeader).Parse(ctx)
+ current, _, _, _, currentErr := s.view.session.cache.ParseGoHandle(currentFH, source.ParseHeader).Parse(ctx)
+ if originalErr != nil || currentErr != nil {
+ return (originalErr == nil) != (currentErr == nil)
+ }
+ // Check if the package's metadata has changed. The cases handled are:
+ // 1. A package's name has changed
+ // 2. A file's imports have changed
+ if original.Name.Name != current.Name.Name {
+ return true
+ }
+ // If the package's imports have increased, definitely re-run `go list`.
+ if len(original.Imports) < len(current.Imports) {
+ return true
+ }
+ importSet := make(map[string]struct{})
+ for _, importSpec := range original.Imports {
+ importSet[importSpec.Path.Value] = struct{}{}
+ }
+ // If any of the current imports were not in the original imports.
+ for _, importSpec := range current.Imports {
+ if _, ok := importSet[importSpec.Path.Value]; !ok {
+ return true
+ }
+ }
+ return false
}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 9dbbdf1..2922460 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -10,10 +10,8 @@
"encoding/json"
"fmt"
"go/ast"
- "go/token"
"io"
"io/ioutil"
- stdlog "log"
"os"
"path/filepath"
"reflect"
@@ -23,19 +21,20 @@
"time"
"golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
- "golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
+ "golang.org/x/tools/internal/telemetry/tag"
"golang.org/x/tools/internal/xcontext"
errors "golang.org/x/xerrors"
)
type view struct {
- session *session
+ session *Session
id string
options source.Options
@@ -233,19 +232,20 @@
return astObj, nil
}
-func (v *view) buildBuiltinPackage(ctx context.Context, m *metadata) error {
- if len(m.goFiles) != 1 {
- return errors.Errorf("only expected 1 file, got %v", len(m.goFiles))
+func (v *view) buildBuiltinPackage(ctx context.Context, goFiles []string) error {
+ if len(goFiles) != 1 {
+ return errors.Errorf("only expected 1 file, got %v", len(goFiles))
}
- uri := m.goFiles[0]
+ uri := span.URIFromPath(goFiles[0])
v.addIgnoredFile(uri) // to avoid showing diagnostics for builtin.go
- // Get the FileHandle through the session to avoid adding it to the snapshot.
- pgh := v.session.cache.ParseGoHandle(v.session.GetFile(uri), source.ParseFull)
+ // Get the FileHandle through the cache to avoid adding it to the snapshot
+ // and to get the file content from disk.
+ pgh := v.session.cache.ParseGoHandle(v.session.cache.GetFile(uri), source.ParseFull)
fset := v.session.cache.fset
h := v.session.cache.store.Bind(pgh.File().Identity(), func(ctx context.Context) interface{} {
data := &builtinPackageData{}
- file, _, _, err := pgh.Parse(ctx)
+ file, _, _, _, err := pgh.Parse(ctx)
if err != nil {
data.err = err
return data
@@ -262,44 +262,6 @@
return nil
}
-// Config returns the configuration used for the view's interaction with the
-// go/packages API. It is shared across all views.
-func (v *view) Config(ctx context.Context) *packages.Config {
- // TODO: Should we cache the config and/or overlay somewhere?
-
- // We want to run the go commands with the -modfile flag if the version of go
- // that we are using supports it.
- buildFlags := v.options.BuildFlags
- if v.tempMod != "" {
- buildFlags = append(buildFlags, fmt.Sprintf("-modfile=%s", v.tempMod.Filename()))
- }
- cfg := &packages.Config{
- Dir: v.folder.Filename(),
- Context: ctx,
- BuildFlags: buildFlags,
- Mode: packages.NeedName |
- packages.NeedFiles |
- packages.NeedCompiledGoFiles |
- packages.NeedImports |
- packages.NeedDeps |
- packages.NeedTypesSizes,
- Fset: v.session.cache.fset,
- Overlay: v.session.buildOverlay(),
- ParseFile: func(*token.FileSet, string, []byte) (*ast.File, error) {
- panic("go/packages must not be used to parse files")
- },
- Logf: func(format string, args ...interface{}) {
- if v.options.VerboseOutput {
- log.Print(ctx, fmt.Sprintf(format, args...))
- }
- },
- Tests: true,
- }
- cfg.Env = append(cfg.Env, fmt.Sprintf("GOPATH=%s", v.gopath))
- cfg.Env = append(cfg.Env, v.options.Env...)
- return cfg
-}
-
func (v *view) RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error) error {
v.importsMu.Lock()
defer v.importsMu.Unlock()
@@ -313,8 +275,7 @@
// In module mode, check if the mod file has changed.
if v.realMod != "" {
- mod, err := v.Snapshot().GetFile(v.realMod)
- if err == nil && mod.Identity() != v.cachedModFileVersion {
+ if mod := v.session.cache.GetFile(v.realMod); mod.Identity() != v.cachedModFileVersion {
v.processEnv.GetResolver().(*imports.ModuleResolver).ClearForNewMod()
v.cachedModFileVersion = mod.Identity()
}
@@ -358,9 +319,9 @@
v.importsMu.Unlock()
// We don't have a context handy to use for logging, so use the stdlib for now.
- stdlog.Printf("background imports cache refresh starting")
+ log.Print(v.baseCtx, "background imports cache refresh starting")
err := imports.PrimeCache(context.Background(), env)
- stdlog.Printf("background refresh finished after %v with err: %v", time.Since(start), err)
+ log.Print(v.baseCtx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), tag.Of("Error", err))
v.importsMu.Lock()
v.cacheRefreshDuration = time.Since(start)
@@ -369,17 +330,17 @@
}
func (v *view) buildProcessEnv(ctx context.Context) (*imports.ProcessEnv, error) {
- cfg := v.Config(ctx)
+ env, buildFlags := v.env()
processEnv := &imports.ProcessEnv{
- WorkingDir: cfg.Dir,
- BuildFlags: cfg.BuildFlags,
+ WorkingDir: v.folder.Filename(),
+ BuildFlags: buildFlags,
Logf: func(format string, args ...interface{}) {
log.Print(ctx, fmt.Sprintf(format, args...))
},
LocalPrefix: v.options.LocalPrefix,
Debug: v.options.VerboseOutput,
}
- for _, kv := range cfg.Env {
+ for _, kv := range env {
split := strings.Split(kv, "=")
if len(split) < 2 {
continue
@@ -402,10 +363,16 @@
return processEnv, nil
}
-func (v *view) fileVersion(filename string) string {
- uri := span.FileURI(filename)
- fh := v.session.GetFile(uri)
- return fh.Identity().String()
+func (v *view) env() ([]string, []string) {
+ // We want to run the go commands with the -modfile flag if the version of go
+ // that we are using supports it.
+ buildFlags := v.options.BuildFlags
+ if v.tempMod != "" {
+ buildFlags = append(buildFlags, fmt.Sprintf("-modfile=%s", v.tempMod.Filename()))
+ }
+ env := []string{fmt.Sprintf("GOPATH=%s", v.gopath)}
+ env = append(env, v.options.Env...)
+ return env, buildFlags
}
func (v *view) contains(uri span.URI) bool {
@@ -516,7 +483,7 @@
os.Remove(v.tempMod.Filename())
os.Remove(tempSumFile(v.tempMod.Filename()))
}
- debug.DropView(debugView{v})
+ v.session.cache.debug.DropView(debugView{v})
}
// Ignore checks if the given URI is a URI we ignore.
@@ -565,30 +532,7 @@
v.initializeOnce.Do(func() {
defer close(v.initialized)
- err := func() error {
- // Do not cancel the call to go/packages.Load for the entire workspace.
- meta, err := s.load(ctx, viewLoadScope("LOAD_VIEW"), packagePath("builtin"))
- if err != nil {
- return err
- }
- // Keep track of the workspace packages.
- for _, m := range meta {
- // Make sure to handle the builtin package separately
- // Don't set it as a workspace package.
- if m.pkgPath == "builtin" {
- if err := s.view.buildBuiltinPackage(ctx, m); err != nil {
- return err
- }
- continue
- }
- s.setWorkspacePackage(ctx, m)
- if _, err := s.packageHandle(ctx, m.id); err != nil {
- return err
- }
- }
- return nil
- }()
- if err != nil {
+ if err := s.load(ctx, viewLoadScope("LOAD_VIEW"), packagePath("builtin")); err != nil {
log.Error(ctx, "initial workspace load failed", err)
}
})
@@ -604,7 +548,7 @@
// invalidateContent invalidates the content of a Go file,
// including any position and type information that depends on it.
// It returns true if we were already tracking the given file, false otherwise.
-func (v *view) invalidateContent(ctx context.Context, uris []span.URI) source.Snapshot {
+func (v *view) invalidateContent(ctx context.Context, uris map[span.URI]source.FileHandle) source.Snapshot {
// Detach the context so that content invalidation cannot be canceled.
ctx = xcontext.Detach(ctx)
@@ -641,14 +585,14 @@
if modFile == os.DevNull {
return nil
}
- v.realMod = span.FileURI(modFile)
+ v.realMod = span.URIFromPath(modFile)
// Now that we have set all required fields,
// check if the view has a valid build configuration.
v.hasValidBuildConfiguration = checkBuildConfiguration(v.goCommand, v.realMod, v.folder, v.gopath)
- // The user has disabled the use of the -modfile flag.
- if !modfileFlagEnabled {
+ // The user has disabled the use of the -modfile flag or has no go.mod file.
+ if !modfileFlagEnabled || v.realMod == "" {
return nil
}
if modfileFlag, err := v.modfileFlagExists(ctx, v.Options().Env); err != nil {
@@ -657,9 +601,10 @@
return nil
}
// Copy the current go.mod file into the temporary go.mod file.
- // The file's name will be of the format go.1234.mod.
- // It's temporary go.sum file should have the corresponding format of go.1234.sum.
- tempModFile, err := ioutil.TempFile("", "go.*.mod")
+ // The file's name will be of the format go.directory.1234.mod.
+ // It's temporary go.sum file should have the corresponding format of go.directory.1234.sum.
+ tmpPattern := fmt.Sprintf("go.%s.*.mod", filepath.Base(folder.Filename()))
+ tempModFile, err := ioutil.TempFile("", tmpPattern)
if err != nil {
return err
}
@@ -674,7 +619,7 @@
if _, err := io.Copy(tempModFile, origFile); err != nil {
return err
}
- v.tempMod = span.FileURI(tempModFile.Name())
+ v.tempMod = span.URIFromPath(tempModFile.Name())
// Copy go.sum file as well (if there is one).
sumFile := filepath.Join(filepath.Dir(modFile), "go.sum")
@@ -741,12 +686,18 @@
gopackagesdriver = true
}
}
- b, err := source.InvokeGo(ctx, v.folder.Filename(), env, "env", "-json")
+ inv := gocommand.Invocation{
+ Verb: "env",
+ Args: []string{"-json"},
+ Env: env,
+ WorkingDir: v.Folder().Filename(),
+ }
+ stdout, err := inv.Run(ctx)
if err != nil {
return "", err
}
envMap := make(map[string]string)
- decoder := json.NewDecoder(b)
+ decoder := json.NewDecoder(stdout)
if err := decoder.Decode(&envMap); err != nil {
return "", err
}
@@ -825,7 +776,13 @@
// Borrowed from internal/imports/mod.go:620.
const format = `{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}`
folder := v.folder.Filename()
- stdout, err := source.InvokeGo(ctx, folder, append(env, "GO111MODULE=off"), "list", "-e", "-f", format)
+ inv := gocommand.Invocation{
+ Verb: "list",
+ Args: []string{"-e", "-f", format},
+ Env: append(env, "GO111MODULE=off"),
+ WorkingDir: v.Folder().Filename(),
+ }
+ stdout, err := inv.Run(ctx)
if err != nil {
return false, err
}
diff --git a/internal/lsp/cmd/capabilities_test.go b/internal/lsp/cmd/capabilities_test.go
index 4d15a2e..e966166 100644
--- a/internal/lsp/cmd/capabilities_test.go
+++ b/internal/lsp/cmd/capabilities_test.go
@@ -10,7 +10,6 @@
"golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
)
@@ -36,11 +35,11 @@
defer c.terminate(ctx)
params := &protocol.ParamInitialize{}
- params.RootURI = string(span.FileURI(c.Client.app.wd))
+ params.RootURI = protocol.URIFromPath(c.Client.app.wd)
params.Capabilities.Workspace.Configuration = true
// Send an initialize request to the server.
- ctx, c.Server = lsp.NewClientServer(ctx, cache.New(app.options).NewSession(), c.Client)
+ c.Server = lsp.NewServer(cache.New(app.options, nil).NewSession(), c.Client)
result, err := c.Server.Initialize(ctx, params)
if err != nil {
t.Fatal(err)
@@ -55,7 +54,7 @@
}
// Open the file on the server side.
- uri := protocol.NewURI(span.FileURI(tmpFile))
+ uri := protocol.URIFromPath(tmpFile)
if err := c.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{
URI: uri,
diff --git a/internal/lsp/cmd/check.go b/internal/lsp/cmd/check.go
index 3331053..7d22db8 100644
--- a/internal/lsp/cmd/check.go
+++ b/internal/lsp/cmd/check.go
@@ -48,7 +48,7 @@
}
defer conn.terminate(ctx)
for _, arg := range args {
- uri := span.FileURI(arg)
+ uri := span.URIFromPath(arg)
uris = append(uris, uri)
file := conn.AddFile(ctx, uri)
if file.err != nil {
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index e0eefaf..aeb8bd0 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -22,11 +22,10 @@
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/cache"
+ "golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
- "golang.org/x/tools/internal/telemetry/export"
- "golang.org/x/tools/internal/telemetry/export/ocagent"
"golang.org/x/tools/internal/tool"
"golang.org/x/tools/internal/xcontext"
errors "golang.org/x/xerrors"
@@ -58,7 +57,7 @@
env []string
// Support for remote lsp server
- Remote string `flag:"remote" help:"*EXPERIMENTAL* - forward all commands to a remote lsp"`
+ Remote string `flag:"remote" help:"*EXPERIMENTAL* - forward all commands to a remote lsp specified by this flag. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto', the remote address is automatically resolved based on the executing environment. Otherwise, TCP is used."`
// Enable verbose logging
Verbose bool `flag:"v" help:"verbose output"`
@@ -69,6 +68,8 @@
// PrepareOptions is called to update the options when a new view is built.
// It is primarily to allow the behavior of gopls to be modified by hooks.
PrepareOptions func(*source.Options)
+
+ debug *debug.Instance
}
// New returns a new Application ready to run.
@@ -130,10 +131,7 @@
// If no arguments are passed it will invoke the server sub command, as a
// temporary measure for compatibility.
func (app *Application) Run(ctx context.Context, args ...string) error {
- ocConfig := ocagent.Discover()
- //TODO: we should not need to adjust the discovered configuration
- ocConfig.Address = app.OCAgent
- export.AddExporters(ocagent.Connect(ocConfig))
+ app.debug = debug.NewInstance(app.wd, app.OCAgent)
app.Serve.app = app
if len(args) == 0 {
return tool.Run(ctx, &app.Serve, args)
@@ -174,6 +172,7 @@
&implementation{app: app},
&imports{app: app},
&links{app: app},
+ &prepareRename{app: app},
&query{app: app},
&references{app: app},
&rename{app: app},
@@ -189,54 +188,54 @@
)
func (app *Application) connect(ctx context.Context) (*connection, error) {
- switch app.Remote {
- case "":
+ switch {
+ case app.Remote == "":
connection := newConnection(app)
- ctx, connection.Server = lsp.NewClientServer(ctx, cache.New(app.options).NewSession(), connection.Client)
+ connection.Server = lsp.NewServer(cache.New(app.options, nil).NewSession(), connection.Client)
+ ctx = protocol.WithClient(ctx, connection.Client)
return connection, connection.initialize(ctx, app.options)
- case "internal":
+ case strings.HasPrefix(app.Remote, "internal@"):
internalMu.Lock()
defer internalMu.Unlock()
if c := internalConnections[app.wd]; c != nil {
return c, nil
}
- connection := newConnection(app)
+ remote := app.Remote[len("internal@"):]
ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server
- cr, sw, _ := os.Pipe()
- sr, cw, _ := os.Pipe()
- var jc *jsonrpc2.Conn
- ctx, jc, connection.Server = protocol.NewClient(ctx, jsonrpc2.NewHeaderStream(cr, cw), connection.Client)
- go jc.Run(ctx)
- go func() {
- ctx, srv := lsp.NewServer(ctx, cache.New(app.options).NewSession(), jsonrpc2.NewHeaderStream(sr, sw))
- srv.Run(ctx)
- }()
- if err := connection.initialize(ctx, app.options); err != nil {
+ connection, err := app.connectRemote(ctx, remote)
+ if err != nil {
return nil, err
}
internalConnections[app.wd] = connection
return connection, nil
default:
- connection := newConnection(app)
- conn, err := net.Dial("tcp", app.Remote)
- if err != nil {
- return nil, err
- }
- stream := jsonrpc2.NewHeaderStream(conn, conn)
- var jc *jsonrpc2.Conn
- ctx, jc, connection.Server = protocol.NewClient(ctx, stream, connection.Client)
- go jc.Run(ctx)
- return connection, connection.initialize(ctx, app.options)
+ return app.connectRemote(ctx, app.Remote)
}
}
+func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) {
+ connection := newConnection(app)
+ conn, err := net.Dial("tcp", remote)
+ if err != nil {
+ return nil, err
+ }
+ stream := jsonrpc2.NewHeaderStream(conn, conn)
+ cc := jsonrpc2.NewConn(stream)
+ connection.Server = protocol.ServerDispatcher(cc)
+ cc.AddHandler(protocol.ClientHandler(connection.Client))
+ cc.AddHandler(protocol.Canceller{})
+ ctx = protocol.WithClient(ctx, connection.Client)
+ go cc.Run(ctx)
+ return connection, connection.initialize(ctx, app.options)
+}
+
func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error {
params := &protocol.ParamInitialize{}
- params.RootURI = string(span.FileURI(c.Client.app.wd))
+ params.RootURI = protocol.URIFromPath(c.Client.app.wd)
params.Capabilities.Workspace.Configuration = true
// Make sure to respect configured options when sending initialize request.
- opts := source.DefaultOptions
+ opts := source.DefaultOptions()
if options != nil {
options(&opts)
}
@@ -288,6 +287,15 @@
}
}
+// fileURI converts a DocumentURI to a file:// span.URI, panicking if it's not a file.
+func fileURI(uri protocol.DocumentURI) span.URI {
+ sURI := uri.SpanURI()
+ if !sURI.IsFile() {
+ panic(fmt.Sprintf("%q is not a file URI", uri))
+ }
+ return sURI
+}
+
func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil }
func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
@@ -368,8 +376,7 @@
c.filesMu.Lock()
defer c.filesMu.Unlock()
- uri := span.URI(p.URI)
- file := c.getFile(ctx, uri)
+ file := c.getFile(ctx, fileURI(p.URI))
file.diagnostics = p.Diagnostics
return nil
}
@@ -419,7 +426,7 @@
file.added = true
p := &protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
LanguageID: source.DetectLanguage("", file.uri.Filename()).String(),
Version: 1,
Text: string(file.mapper.Content),
@@ -446,7 +453,7 @@
}
func (c *connection) terminate(ctx context.Context) {
- if c.Client.app.Remote == "internal" {
+ if strings.HasPrefix(c.Client.app.Remote, "internal@") {
// internal connections need to be left alive for the next test
return
}
diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go
index 0e417c6..bc56a3a 100644
--- a/internal/lsp/cmd/cmd_test.go
+++ b/internal/lsp/cmd/cmd_test.go
@@ -5,6 +5,7 @@
package cmd_test
import (
+ "context"
"fmt"
"os"
"path/filepath"
@@ -13,8 +14,12 @@
"testing"
"golang.org/x/tools/go/packages/packagestest"
+ "golang.org/x/tools/internal/jsonrpc2/servertest"
+ "golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/cmd"
cmdtest "golang.org/x/tools/internal/lsp/cmd/test"
+ "golang.org/x/tools/internal/lsp/debug"
+ "golang.org/x/tools/internal/lsp/lsprpc"
"golang.org/x/tools/internal/lsp/tests"
"golang.org/x/tools/internal/testenv"
)
@@ -29,9 +34,23 @@
}
func testCommandLine(t *testing.T, exporter packagestest.Exporter) {
+ ctx := tests.Context(t)
+ ts := testServer(ctx)
data := tests.Load(t, exporter, "../testdata")
- defer data.Exported.Cleanup()
- tests.Run(t, cmdtest.NewRunner(exporter, data, tests.Context(t), nil), data)
+ for _, datum := range data {
+ defer datum.Exported.Cleanup()
+ t.Run(datum.Folder, func(t *testing.T) {
+ t.Helper()
+ tests.Run(t, cmdtest.NewRunner(exporter, datum, ctx, ts.Addr, nil), datum)
+ })
+ }
+}
+
+func testServer(ctx context.Context) *servertest.TCPServer {
+ di := debug.NewInstance("", "")
+ cache := cache.New(nil, di.State)
+ ss := lsprpc.NewStreamServer(cache, false, di)
+ return servertest.NewTCPServer(ctx, ss)
}
func TestDefinitionHelpExample(t *testing.T) {
@@ -45,6 +64,8 @@
t.Errorf("could not get wd: %v", err)
return
}
+ ctx := tests.Context(t)
+ ts := testServer(ctx)
thisFile := filepath.Join(dir, "definition.go")
baseArgs := []string{"query", "definition"}
expect := regexp.MustCompile(`(?s)^[\w/\\:_-]+flag[/\\]flag.go:\d+:\d+-\d+: defined here as FlagSet struct {.*}$`)
@@ -52,7 +73,7 @@
fmt.Sprintf("%v:%v:%v", thisFile, cmd.ExampleLine, cmd.ExampleColumn),
fmt.Sprintf("%v:#%v", thisFile, cmd.ExampleOffset)} {
args := append(baseArgs, query)
- r := cmdtest.NewRunner(nil, nil, tests.Context(t), nil)
+ r := cmdtest.NewRunner(nil, nil, ctx, ts.Addr, nil)
got, _ := r.NormalizeGoplsCmd(t, args...)
if !expect.MatchString(got) {
t.Errorf("test with %v\nexpected:\n%s\ngot:\n%s", args, expect, got)
diff --git a/internal/lsp/cmd/definition.go b/internal/lsp/cmd/definition.go
index 5cabac2..2b2e3c0 100644
--- a/internal/lsp/cmd/definition.go
+++ b/internal/lsp/cmd/definition.go
@@ -109,7 +109,7 @@
if hover == nil {
return errors.Errorf("%v: not an identifier", from)
}
- file = conn.AddFile(ctx, span.NewURI(locs[0].URI))
+ file = conn.AddFile(ctx, fileURI(locs[0].URI))
if file.err != nil {
return errors.Errorf("%v: %v", from, file.err)
}
@@ -134,9 +134,6 @@
default:
return errors.Errorf("unknown emulation for definition: %s", d.query.Emulate)
}
- if err != nil {
- return err
- }
if d.query.JSON {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "\t")
diff --git a/internal/lsp/cmd/folding_range.go b/internal/lsp/cmd/folding_range.go
index d6e3b73..f655f30 100644
--- a/internal/lsp/cmd/folding_range.go
+++ b/internal/lsp/cmd/folding_range.go
@@ -50,7 +50,7 @@
p := protocol.FoldingRangeParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(from.URI()),
+ URI: protocol.URIFromSpanURI(from.URI()),
},
}
diff --git a/internal/lsp/cmd/implementation.go b/internal/lsp/cmd/implementation.go
index 98a6b8a..e498372 100644
--- a/internal/lsp/cmd/implementation.go
+++ b/internal/lsp/cmd/implementation.go
@@ -72,7 +72,7 @@
var spans []string
for _, impl := range implementations {
- f := conn.AddFile(ctx, span.NewURI(impl.URI))
+ f := conn.AddFile(ctx, fileURI(impl.URI))
span, err := f.mapper.Span(impl)
if err != nil {
return err
diff --git a/internal/lsp/cmd/imports.go b/internal/lsp/cmd/imports.go
index 2127e25..a6d00e9 100644
--- a/internal/lsp/cmd/imports.go
+++ b/internal/lsp/cmd/imports.go
@@ -62,7 +62,7 @@
}
actions, err := conn.CodeAction(ctx, &protocol.CodeActionParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
})
if err != nil {
@@ -74,7 +74,7 @@
continue
}
for _, c := range a.Edit.DocumentChanges {
- if c.TextDocument.URI == string(uri) {
+ if fileURI(c.TextDocument.URI) == uri {
edits = append(edits, c.Edits...)
}
}
diff --git a/internal/lsp/cmd/links.go b/internal/lsp/cmd/links.go
index a93ae8f..1d5a669 100644
--- a/internal/lsp/cmd/links.go
+++ b/internal/lsp/cmd/links.go
@@ -59,7 +59,7 @@
}
results, err := conn.DocumentLink(ctx, &protocol.DocumentLinkParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
})
if err != nil {
diff --git a/internal/lsp/cmd/prepare_rename.go b/internal/lsp/cmd/prepare_rename.go
new file mode 100644
index 0000000..40ee201
--- /dev/null
+++ b/internal/lsp/cmd/prepare_rename.go
@@ -0,0 +1,80 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cmd
+
+import (
+ "context"
+ "flag"
+ "fmt"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/tool"
+)
+
+// prepareRename implements the prepare_rename verb for gopls.
+type prepareRename struct {
+ app *Application
+}
+
+func (r *prepareRename) Name() string { return "prepare_rename" }
+func (r *prepareRename) Usage() string { return "<position>" }
+func (r *prepareRename) ShortHelp() string { return "test validity of a rename operation at location" }
+func (r *prepareRename) DetailedHelp(f *flag.FlagSet) {
+ fmt.Fprint(f.Output(), `
+Example:
+
+ $ # 1-indexed location (:line:column or :#offset) of the target identifier
+ $ gopls prepare_rename helper/helper.go:8:6
+ $ gopls prepare_rename helper/helper.go:#53
+
+ gopls prepare_rename flags are:
+`)
+ f.PrintDefaults()
+}
+
+func (r *prepareRename) Run(ctx context.Context, args ...string) error {
+ if len(args) != 1 {
+ return tool.CommandLineErrorf("prepare_rename expects 1 argument (file)")
+ }
+
+ conn, err := r.app.connect(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.terminate(ctx)
+
+ from := span.Parse(args[0])
+ file := conn.AddFile(ctx, from.URI())
+ if file.err != nil {
+ return file.err
+ }
+ loc, err := file.mapper.Location(from)
+ if err != nil {
+ return err
+ }
+ p := protocol.PrepareRenameParams{
+ TextDocumentPositionParams: protocol.TextDocumentPositionParams{
+ TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
+ Position: loc.Range.Start,
+ },
+ }
+ result, err := conn.PrepareRename(ctx, &p)
+ if err != nil {
+ return fmt.Errorf("prepare_rename failed: %v", err)
+ }
+ if result == nil {
+ return fmt.Errorf("request is not valid at the given position")
+ }
+
+ l := protocol.Location{Range: *result}
+ s, err := file.mapper.Span(l)
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(s)
+ return nil
+}
diff --git a/internal/lsp/cmd/references.go b/internal/lsp/cmd/references.go
index cc85e79..5626019 100644
--- a/internal/lsp/cmd/references.go
+++ b/internal/lsp/cmd/references.go
@@ -73,7 +73,7 @@
}
var spans []string
for _, l := range locations {
- f := conn.AddFile(ctx, span.NewURI(l.URI))
+ f := conn.AddFile(ctx, fileURI(l.URI))
// convert location to span for user-friendly 1-indexed line
// and column numbers
span, err := f.mapper.Span(l)
diff --git a/internal/lsp/cmd/rename.go b/internal/lsp/cmd/rename.go
index 42eeeaa..57cf846 100644
--- a/internal/lsp/cmd/rename.go
+++ b/internal/lsp/cmd/rename.go
@@ -81,15 +81,15 @@
var orderedURIs []string
edits := map[span.URI][]protocol.TextEdit{}
for _, c := range edit.DocumentChanges {
- uri := span.URI(c.TextDocument.URI)
+ uri := fileURI(c.TextDocument.URI)
edits[uri] = append(edits[uri], c.Edits...)
- orderedURIs = append(orderedURIs, c.TextDocument.URI)
+ orderedURIs = append(orderedURIs, string(uri))
}
sort.Strings(orderedURIs)
changeCount := len(orderedURIs)
for _, u := range orderedURIs {
- uri := span.URI(u)
+ uri := span.URIFromURI(u)
cmdFile := conn.AddFile(ctx, uri)
filename := cmdFile.uri.Filename()
diff --git a/internal/lsp/cmd/serve.go b/internal/lsp/cmd/serve.go
index 02b71f0..17ee9e3 100644
--- a/internal/lsp/cmd/serve.go
+++ b/internal/lsp/cmd/serve.go
@@ -6,36 +6,29 @@
import (
"context"
- "encoding/json"
"flag"
"fmt"
- "io"
- "log"
- "net"
"os"
- "path/filepath"
+ "strings"
"time"
"golang.org/x/tools/internal/jsonrpc2"
- "golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/cache"
- "golang.org/x/tools/internal/lsp/debug"
+ "golang.org/x/tools/internal/lsp/lsprpc"
"golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/lsp/telemetry"
- "golang.org/x/tools/internal/telemetry/trace"
"golang.org/x/tools/internal/tool"
- errors "golang.org/x/xerrors"
)
// Serve is a struct that exposes the configurable parts of the LSP server as
// flags, in the right form for tool.Main to consume.
type Serve struct {
- Logfile string `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"`
- Mode string `flag:"mode" help:"no effect"`
- Port int `flag:"port" help:"port on which to run gopls for debugging purposes"`
- Address string `flag:"listen" help:"address on which to listen for remote connections"`
- Trace bool `flag:"rpc.trace" help:"print the full rpc trace in lsp inspector format"`
- Debug string `flag:"debug" help:"serve debug information on the supplied address"`
+ Logfile string `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"`
+ Mode string `flag:"mode" help:"no effect"`
+ Port int `flag:"port" help:"port on which to run gopls for debugging purposes"`
+ Address string `flag:"listen" help:"address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used."`
+ IdleTimeout time.Duration `flag:"listen.timeout" help:"when used with -listen, shut down the server when there are no connected clients for this duration"`
+ Trace bool `flag:"rpc.trace" help:"print the full rpc trace in lsp inspector format"`
+ Debug string `flag:"debug" help:"serve debug information on the supplied address"`
app *Application
}
@@ -61,176 +54,49 @@
if len(args) > 0 {
return tool.CommandLineErrorf("server does not take arguments, got %v", args)
}
- out := os.Stderr
- logfile := s.Logfile
- if logfile != "" {
- if logfile == "auto" {
- logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.log", os.Getpid()))
- }
- f, err := os.Create(logfile)
- if err != nil {
- return errors.Errorf("Unable to create log file: %v", err)
- }
- defer f.Close()
- log.SetOutput(io.MultiWriter(os.Stderr, f))
- out = f
- }
- debug.Serve(ctx, s.Debug, debugServe{s: s, logfile: logfile, start: time.Now()})
-
- if s.app.Remote != "" {
- return s.forward()
- }
-
- prepare := func(ctx context.Context, srv *lsp.Server) *lsp.Server {
- srv.Conn.AddHandler(&handler{})
- return srv
- }
- run := func(ctx context.Context, srv *lsp.Server) { go prepare(ctx, srv).Run(ctx) }
- if s.Address != "" {
- return lsp.RunServerOnAddress(ctx, cache.New(s.app.options), s.Address, run)
- }
- if s.Port != 0 {
- return lsp.RunServerOnPort(ctx, cache.New(s.app.options), s.Port, run)
- }
- stream := jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout)
- if s.Trace {
- stream = protocol.LoggingStream(stream, out)
- }
- ctx, srv := lsp.NewServer(ctx, cache.New(s.app.options).NewSession(), stream)
- return prepare(ctx, srv).Run(ctx)
-}
-
-func (s *Serve) forward() error {
- conn, err := net.Dial("tcp", s.app.Remote)
+ closeLog, err := s.app.debug.SetLogFile(s.Logfile)
if err != nil {
return err
}
- errc := make(chan error)
+ defer closeLog()
+ s.app.debug.ServerAddress = s.Address
+ s.app.debug.DebugAddress = s.Debug
+ s.app.debug.Serve(ctx)
+ s.app.debug.MonitorMemory(ctx)
- go func(conn net.Conn) {
- _, err := io.Copy(conn, os.Stdin)
- errc <- err
- }(conn)
-
- go func(conn net.Conn) {
- _, err := io.Copy(os.Stdout, conn)
- errc <- err
- }(conn)
-
- return <-errc
-}
-
-// debugServe implements the debug.Instance interface.
-type debugServe struct {
- s *Serve
- logfile string
- start time.Time
-}
-
-func (d debugServe) Logfile() string { return d.logfile }
-func (d debugServe) StartTime() time.Time { return d.start }
-func (d debugServe) Port() int { return d.s.Port }
-func (d debugServe) Address() string { return d.s.Address }
-func (d debugServe) Debug() string { return d.s.Debug }
-func (d debugServe) Workdir() string { return d.s.app.wd }
-
-type handler struct{}
-
-type rpcStats struct {
- method string
- direction jsonrpc2.Direction
- id *jsonrpc2.ID
- payload *json.RawMessage
- start time.Time
- delivering func()
- close func()
-}
-
-type statsKeyType int
-
-const statsKey = statsKeyType(0)
-
-func (h *handler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
- stats := h.getStats(ctx)
- if stats != nil {
- stats.delivering()
- }
- return false
-}
-
-func (h *handler) Cancel(ctx context.Context, conn *jsonrpc2.Conn, id jsonrpc2.ID, cancelled bool) bool {
- return false
-}
-
-func (h *handler) Request(ctx context.Context, conn *jsonrpc2.Conn, direction jsonrpc2.Direction, r *jsonrpc2.WireRequest) context.Context {
- if r.Method == "" {
- panic("no method in rpc stats")
- }
- stats := &rpcStats{
- method: r.Method,
- start: time.Now(),
- direction: direction,
- payload: r.Params,
- }
- ctx = context.WithValue(ctx, statsKey, stats)
- mode := telemetry.Outbound
- if direction == jsonrpc2.Receive {
- mode = telemetry.Inbound
- }
- ctx, stats.close = trace.StartSpan(ctx, r.Method,
- telemetry.Method.Of(r.Method),
- telemetry.RPCDirection.Of(mode),
- telemetry.RPCID.Of(r.ID),
- )
- telemetry.Started.Record(ctx, 1)
- _, stats.delivering = trace.StartSpan(ctx, "queued")
- return ctx
-}
-
-func (h *handler) Response(ctx context.Context, conn *jsonrpc2.Conn, direction jsonrpc2.Direction, r *jsonrpc2.WireResponse) context.Context {
- return ctx
-}
-
-func (h *handler) Done(ctx context.Context, err error) {
- stats := h.getStats(ctx)
- if err != nil {
- ctx = telemetry.StatusCode.With(ctx, "ERROR")
+ var ss jsonrpc2.StreamServer
+ if s.app.Remote != "" {
+ network, addr := parseAddr(s.app.Remote)
+ ss = lsprpc.NewForwarder(network, addr, true, s.app.debug)
} else {
- ctx = telemetry.StatusCode.With(ctx, "OK")
+ ss = lsprpc.NewStreamServer(cache.New(s.app.options, s.app.debug.State), true, s.app.debug)
}
- elapsedTime := time.Since(stats.start)
- latencyMillis := float64(elapsedTime) / float64(time.Millisecond)
- telemetry.Latency.Record(ctx, latencyMillis)
- stats.close()
-}
-func (h *handler) Read(ctx context.Context, bytes int64) context.Context {
- telemetry.SentBytes.Record(ctx, bytes)
- return ctx
-}
-
-func (h *handler) Wrote(ctx context.Context, bytes int64) context.Context {
- telemetry.ReceivedBytes.Record(ctx, bytes)
- return ctx
-}
-
-const eol = "\r\n\r\n\r\n"
-
-func (h *handler) Error(ctx context.Context, err error) {
-}
-
-func (h *handler) getStats(ctx context.Context) *rpcStats {
- stats, ok := ctx.Value(statsKey).(*rpcStats)
- if !ok || stats == nil {
- method, ok := ctx.Value(telemetry.Method).(string)
- if !ok {
- method = "???"
- }
- stats = &rpcStats{
- method: method,
- close: func() {},
- }
+ if s.Address != "" {
+ network, addr := parseAddr(s.Address)
+ return jsonrpc2.ListenAndServe(ctx, network, addr, ss, s.IdleTimeout)
}
- return stats
+ if s.Port != 0 {
+ addr := fmt.Sprintf(":%v", s.Port)
+ return jsonrpc2.ListenAndServe(ctx, "tcp", addr, ss, s.IdleTimeout)
+ }
+ stream := jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout)
+ if s.Trace {
+ stream = protocol.LoggingStream(stream, s.app.debug.LogWriter)
+ }
+ return ss.ServeStream(ctx, stream)
+}
+
+// parseAddr parses the -listen flag in to a network, and address.
+func parseAddr(listen string) (network string, address string) {
+ // Allow passing just -remote=auto, as a shorthand for using automatic remote
+ // resolution.
+ if listen == lsprpc.AutoNetwork {
+ return lsprpc.AutoNetwork, ""
+ }
+ if parts := strings.SplitN(listen, ";", 2); len(parts) == 2 {
+ return parts[0], parts[1]
+ }
+ return "tcp", listen
}
diff --git a/internal/lsp/cmd/serve_test.go b/internal/lsp/cmd/serve_test.go
index ad3f8b2..7b3bc9a 100644
--- a/internal/lsp/cmd/serve_test.go
+++ b/internal/lsp/cmd/serve_test.go
@@ -1,86 +1,28 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
package cmd
-import (
- "context"
- "io"
- "regexp"
- "testing"
- "time"
+import "testing"
- "golang.org/x/tools/internal/jsonrpc2"
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/telemetry/log"
-)
-
-type fakeServer struct {
- protocol.Server
- client protocol.Client
-}
-
-func (s *fakeServer) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
- // Our instrumentation should cause this message to be logged back to the LSP
- // client.
- log.Print(ctx, "ping")
- return nil
-}
-
-type fakeClient struct {
- protocol.Client
-
- logs chan string
-}
-
-func (c *fakeClient) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error {
- c.logs <- params.Message
- return nil
-}
-
-func TestClientLogging(t *testing.T) {
- server := &fakeServer{}
- client := &fakeClient{logs: make(chan string)}
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- // Bind our fake client and server.
- // sReader and sWriter read from and write to the server. cReader and cWriter
- // read from and write to the client.
- sReader, sWriter := io.Pipe()
- cReader, cWriter := io.Pipe()
- close := func() {
- failOnErr := func(err error) {
- if err != nil {
- t.Fatal(err)
- }
- }
- failOnErr(sReader.Close())
- failOnErr(cReader.Close())
- failOnErr(sWriter.Close())
- failOnErr(cWriter.Close())
+func TestListenParsing(t *testing.T) {
+ tests := []struct {
+ input, wantNetwork, wantAddr string
+ }{
+ {"127.0.0.1:0", "tcp", "127.0.0.1:0"},
+ {"unix;/tmp/sock", "unix", "/tmp/sock"},
+ {"auto", "auto", ""},
+ {"auto;foo", "auto", "foo"},
}
- defer close()
- serverStream := jsonrpc2.NewStream(sReader, cWriter)
- // The returned client dispatches to the client, but it is already stored
- // in the context by NewServer, so we can ignore it.
- serverCtx, serverConn, _ := protocol.NewServer(ctx, serverStream, server)
- serverConn.AddHandler(&handler{})
- clientStream := jsonrpc2.NewStream(cReader, sWriter)
- clientCtx, clientConn, serverDispatch := protocol.NewClient(ctx, clientStream, client)
- go clientConn.Run(clientCtx)
- go serverConn.Run(serverCtx)
- serverDispatch.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{})
-
- select {
- case got := <-client.logs:
- want := "ping"
- matched, err := regexp.MatchString(want, got)
- if err != nil {
- t.Fatal(err)
+ for _, test := range tests {
+ gotNetwork, gotAddr := parseAddr(test.input)
+ if gotNetwork != test.wantNetwork {
+ t.Errorf("network = %q, want %q", gotNetwork, test.wantNetwork)
}
- if !matched {
- t.Errorf("got log %q, want a log containing %q", got, want)
+ if gotAddr != test.wantAddr {
+ t.Errorf("addr = %q, want %q", gotAddr, test.wantAddr)
}
- case <-time.After(1 * time.Second):
- t.Error("timeout waiting for client log")
}
}
diff --git a/internal/lsp/cmd/signature.go b/internal/lsp/cmd/signature.go
index 7cc91cd..2cec976 100644
--- a/internal/lsp/cmd/signature.go
+++ b/internal/lsp/cmd/signature.go
@@ -59,7 +59,7 @@
tdpp := protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(from.URI()),
+ URI: protocol.URIFromSpanURI(from.URI()),
},
Position: loc.Range.Start,
}
diff --git a/internal/lsp/cmd/suggested_fix.go b/internal/lsp/cmd/suggested_fix.go
index 19a53dc..5e8b1fa 100644
--- a/internal/lsp/cmd/suggested_fix.go
+++ b/internal/lsp/cmd/suggested_fix.go
@@ -70,7 +70,7 @@
p := protocol.CodeActionParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
Context: protocol.CodeActionContext{
Only: []protocol.CodeActionKind{protocol.QuickFix},
@@ -87,7 +87,7 @@
continue
}
for _, c := range a.Edit.DocumentChanges {
- if c.TextDocument.URI == string(uri) {
+ if fileURI(c.TextDocument.URI) == uri {
edits = append(edits, c.Edits...)
}
}
diff --git a/internal/lsp/cmd/symbols.go b/internal/lsp/cmd/symbols.go
index 41cc0f7..eb3aa02 100644
--- a/internal/lsp/cmd/symbols.go
+++ b/internal/lsp/cmd/symbols.go
@@ -44,7 +44,7 @@
from := span.Parse(args[0])
p := protocol.DocumentSymbolParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: string(from.URI()),
+ URI: protocol.URIFromSpanURI(from.URI()),
},
}
diff --git a/internal/lsp/cmd/test/check.go b/internal/lsp/cmd/test/check.go
index db277fa..f21194f 100644
--- a/internal/lsp/cmd/test/check.go
+++ b/internal/lsp/cmd/test/check.go
@@ -22,7 +22,7 @@
t.Skip("skipping circular diagnostics tests due to golang/go#36265")
}
fname := uri.Filename()
- out, _ := r.RunGoplsCmd(t, "check", fname)
+ out, _ := r.runGoplsCmd(t, "check", fname)
// parse got into a collection of reports
got := map[string]struct{}{}
for _, l := range strings.Split(out, "\n") {
diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go
index f0aab4b..7754610 100644
--- a/internal/lsp/cmd/test/cmdtest.go
+++ b/internal/lsp/cmd/test/cmdtest.go
@@ -9,15 +9,17 @@
"bytes"
"context"
"fmt"
- "io/ioutil"
+ "io"
"os"
"path/filepath"
"strconv"
"strings"
+ "sync"
"testing"
"golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/lsp/cmd"
+ "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/tests"
"golang.org/x/tools/internal/span"
@@ -30,6 +32,7 @@
ctx context.Context
options func(*source.Options)
normalizers []normalizer
+ remote string
}
type normalizer struct {
@@ -39,13 +42,14 @@
fragment string
}
-func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Context, options func(*source.Options)) *runner {
+func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner {
r := &runner{
exporter: exporter,
data: data,
ctx: ctx,
options: options,
normalizers: make([]normalizer, 0, len(data.Exported.Modules)),
+ remote: remote,
}
// build the path normalizing patterns
for _, m := range data.Exported.Modules {
@@ -67,6 +71,10 @@
return r
}
+func (r *runner) CodeLens(t *testing.T, spn span.Span, want []protocol.CodeLens) {
+ //TODO: add command line completions tests when it works
+}
+
func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
//TODO: add command line completions tests when it works
}
@@ -95,11 +103,19 @@
//TODO: add command line completions tests when it works
}
-func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) {
- //TODO: add command line prepare rename tests when it works
+func (r *runner) WorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) {
+ //TODO: add command line workspace symbol tests when it works
}
-func (r *runner) RunGoplsCmd(t testing.TB, args ...string) (string, string) {
+func (r *runner) FuzzyWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) {
+ //TODO: add command line workspace symbol tests when it works
+}
+
+func (r *runner) CaseSensitiveWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) {
+ //TODO: add command line workspace symbol tests when it works
+}
+
+func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
rStdout, wStdout, err := os.Pipe()
if err != nil {
t.Fatal(err)
@@ -110,39 +126,38 @@
t.Fatal(err)
}
oldStderr := os.Stderr
- defer func() {
- os.Stdout = oldStdout
- os.Stderr = oldStderr
- wStdout.Close()
- rStdout.Close()
- wStderr.Close()
- rStderr.Close()
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
+ var wg sync.WaitGroup
+ wg.Add(2)
+ go func() {
+ io.Copy(stdout, rStdout)
+ wg.Done()
}()
- os.Stdout = wStdout
- os.Stderr = wStderr
+ go func() {
+ io.Copy(stderr, rStderr)
+ wg.Done()
+ }()
+ os.Stdout, os.Stderr = wStdout, wStderr
app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options)
+ remote := r.remote
err = tool.Run(tests.Context(t),
app,
- append([]string{"-remote=internal"}, args...))
+ append([]string{fmt.Sprintf("-remote=internal@%s", remote)}, args...))
if err != nil {
fmt.Fprint(os.Stderr, err)
}
wStdout.Close()
wStderr.Close()
- stdout, err := ioutil.ReadAll(rStdout)
- if err != nil {
- t.Fatal(err)
- }
- stderr, err := ioutil.ReadAll(rStderr)
- if err != nil {
- t.Fatal(err)
- }
- return string(stdout), string(stderr)
+ wg.Wait()
+ os.Stdout, os.Stderr = oldStdout, oldStderr
+ rStdout.Close()
+ rStderr.Close()
+ return stdout.String(), stderr.String()
}
// NormalizeGoplsCmd runs the gopls command and normalizes its output.
func (r *runner) NormalizeGoplsCmd(t testing.TB, args ...string) (string, string) {
- stdout, stderr := r.RunGoplsCmd(t, args...)
+ stdout, stderr := r.runGoplsCmd(t, args...)
return r.Normalize(stdout), r.Normalize(stderr)
}
diff --git a/internal/lsp/cmd/test/prepare_rename.go b/internal/lsp/cmd/test/prepare_rename.go
new file mode 100644
index 0000000..3d69edd
--- /dev/null
+++ b/internal/lsp/cmd/test/prepare_rename.go
@@ -0,0 +1,45 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cmdtest
+
+import (
+ "fmt"
+ "testing"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/span"
+)
+
+func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) {
+ m, err := r.data.Mapper(src.URI())
+ if err != nil {
+ t.Errorf("prepare_rename failed: %v", err)
+ }
+
+ var (
+ target = fmt.Sprintf("%v", src)
+ args = []string{"prepare_rename", target}
+ stdOut, stdErr = r.NormalizeGoplsCmd(t, args...)
+ expect string
+ )
+
+ if want.Text == "" {
+ if stdErr != "" {
+ t.Errorf("prepare_rename failed for %s,\nexpected:\n`%v`\ngot:\n`%v`", target, expect, stdErr)
+ }
+ return
+ }
+
+ ws, err := m.Span(protocol.Location{Range: want.Range})
+ if err != nil {
+ t.Errorf("prepare_rename failed: %v", err)
+ }
+
+ expect = r.Normalize(fmt.Sprintln(ws))
+ if expect != stdOut {
+ t.Errorf("prepare_rename failed for %s expected:\n`%s`\ngot:\n`%s`\n", target, expect, stdOut)
+ }
+}
diff --git a/internal/lsp/cmd/test/signature.go b/internal/lsp/cmd/test/signature.go
index 4e2726c..0c77da1 100644
--- a/internal/lsp/cmd/test/signature.go
+++ b/internal/lsp/cmd/test/signature.go
@@ -8,20 +8,22 @@
"fmt"
"testing"
- "golang.org/x/tools/internal/lsp/source"
-
+ "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
)
-func (r *runner) SignatureHelp(t *testing.T, spn span.Span, expectedSignature *source.SignatureInformation) {
- goldenTag := "-signature"
- if expectedSignature != nil {
- goldenTag = expectedSignature.Label + goldenTag
- }
+func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) {
uri := spn.URI()
filename := uri.Filename()
target := filename + fmt.Sprintf(":%v:%v", spn.Start().Line(), spn.Start().Column())
got, _ := r.NormalizeGoplsCmd(t, "signature", target)
+ if want == nil {
+ if got != "" {
+ t.Fatalf("want nil, but got %s", got)
+ }
+ return
+ }
+ goldenTag := want.Signatures[0].Label + "-signature"
expect := string(r.data.Golden(goldenTag, filename, func() ([]byte, error) {
return []byte(got), nil
}))
diff --git a/internal/lsp/cmd/test/suggested_fix.go b/internal/lsp/cmd/test/suggested_fix.go
index f88131b..1963fdb 100644
--- a/internal/lsp/cmd/test/suggested_fix.go
+++ b/internal/lsp/cmd/test/suggested_fix.go
@@ -7,6 +7,7 @@
import (
"testing"
+ "golang.org/x/tools/internal/lsp/tests"
"golang.org/x/tools/internal/span"
)
@@ -14,7 +15,7 @@
uri := spn.URI()
filename := uri.Filename()
got, _ := r.NormalizeGoplsCmd(t, "fix", "-a", filename)
- want := string(r.data.Golden("suggestedfix", filename, func() ([]byte, error) {
+ want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), filename, func() ([]byte, error) {
return []byte(got), nil
}))
if want != got {
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index c7f99ce..c9f85fd 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -15,25 +15,19 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
- "golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
errors "golang.org/x/xerrors"
)
func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
+ uri := fh.Identity().URI
// Determine the supported actions for this file kind.
- supportedCodeActions, ok := view.Options().SupportedCodeActions[fh.Identity().Kind]
+ supportedCodeActions, ok := snapshot.View().Options().SupportedCodeActions[fh.Identity().Kind]
if !ok {
return nil, fmt.Errorf("no supported code actions for %v file kind", fh.Identity().Kind)
}
@@ -56,6 +50,9 @@
var codeActions []protocol.CodeAction
switch fh.Identity().Kind {
case source.Mod:
+ if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
+ codeActions = append(codeActions, mod.SuggestedFixes(ctx, snapshot, fh, diagnostics)...)
+ }
if !wanted[protocol.SourceOrganizeImports] {
codeActions = append(codeActions, protocol.CodeAction{
Title: "Tidy",
@@ -67,9 +64,6 @@
},
})
}
- if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
- codeActions = append(codeActions, mod.SuggestedFixes(ctx, snapshot, fh, diagnostics)...)
- }
case source.Go:
edits, editsPerFix, err := source.AllImportsFixes(ctx, snapshot, fh)
if err != nil {
@@ -101,6 +95,13 @@
}
}
}
+ actions, err := mod.SuggestedGoFixes(ctx, snapshot, fh, diagnostics)
+ if err != nil {
+ log.Error(ctx, "quick fixes failed", err, telemetry.File.Of(uri))
+ }
+ if len(actions) > 0 {
+ codeActions = append(codeActions, actions...)
+ }
}
if wanted[protocol.SourceOrganizeImports] && len(edits) > 0 {
codeActions = append(codeActions, protocol.CodeAction{
@@ -250,7 +251,7 @@
TextDocument: protocol.VersionedTextDocumentIdentifier{
Version: fh.Identity().Version,
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(fh.Identity().URI),
+ URI: protocol.URIFromSpanURI(fh.Identity().URI),
},
},
Edits: edits,
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 6e94a58..4c0ad87 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -3,9 +3,9 @@
import (
"context"
+ "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
)
@@ -15,23 +15,39 @@
if len(params.Arguments) == 0 || len(params.Arguments) > 1 {
return nil, errors.Errorf("expected one file URI for call to `go mod tidy`, got %v", params.Arguments)
}
- // Confirm that this action is being taken on a go.mod file.
- uri := span.NewURI(params.Arguments[0].(string))
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ uri := protocol.DocumentURI(params.Arguments[0].(string))
+ snapshot, _, ok, err := s.beginFileRequest(uri, source.Mod)
+ if !ok {
return nil, err
}
- fh, err := view.Snapshot().GetFile(uri)
- if err != nil {
- return nil, err
- }
- if fh.Identity().Kind != source.Mod {
- return nil, errors.Errorf("%s is not a mod file", uri)
- }
// Run go.mod tidy on the view.
- // TODO: This should go through the ModTidyHandle on the view.
- // That will also allow us to move source.InvokeGo into internal/lsp/cache.
- if _, err := source.InvokeGo(ctx, view.Folder().Filename(), view.Config(ctx).Env, "mod", "tidy"); err != nil {
+ inv := gocommand.Invocation{
+ Verb: "mod",
+ Args: []string{"tidy"},
+ Env: snapshot.Config(ctx).Env,
+ WorkingDir: snapshot.View().Folder().Filename(),
+ }
+ if _, err := inv.Run(ctx); err != nil {
+ return nil, err
+ }
+ case "upgrade.dependency":
+ if len(params.Arguments) < 2 {
+ return nil, errors.Errorf("expected one file URI and one dependency for call to `go get`, got %v", params.Arguments)
+ }
+ uri := protocol.DocumentURI(params.Arguments[0].(string))
+ snapshot, _, ok, err := s.beginFileRequest(uri, source.UnknownKind)
+ if !ok {
+ return nil, err
+ }
+ dep := params.Arguments[1].(string)
+ // Run "go get" on the dependency to upgrade it to the latest version.
+ inv := gocommand.Invocation{
+ Verb: "get",
+ Args: []string{dep},
+ Env: snapshot.Config(ctx).Env,
+ WorkingDir: snapshot.View().Folder().Filename(),
+ }
+ if _, err := inv.Run(ctx); err != nil {
return nil, err
}
}
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index 4ea01b6..ea6af87 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -11,20 +11,13 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/tag"
)
func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
- return nil, err
- }
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+ if !ok {
return nil, err
}
var candidates []source.CompletionItem
@@ -52,7 +45,7 @@
// When using deep completions/fuzzy matching, report results as incomplete so
// client fetches updated completions after every key stroke.
- options := view.Options()
+ options := snapshot.View().Options()
incompleteResults := options.DeepCompletion || options.Matcher == source.Fuzzy
items := toProtocolCompletionItems(candidates, rng, options)
diff --git a/internal/lsp/completion_test.go b/internal/lsp/completion_test.go
index cde7461..93ecb27 100644
--- a/internal/lsp/completion_test.go
+++ b/internal/lsp/completion_test.go
@@ -23,9 +23,7 @@
}
})
- if !strings.Contains(string(src.URI()), "builtins") {
- got = tests.FilterBuiltins(got)
- }
+ got = tests.FilterBuiltins(src, got)
want := expected(t, test, items)
if diff := tests.DiffCompletionItems(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
@@ -51,9 +49,7 @@
func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
got := r.callCompletion(t, src, func(opts *source.Options) {})
- if !strings.Contains(string(src.URI()), "builtins") {
- got = tests.FilterBuiltins(got)
- }
+ got = tests.FilterBuiltins(src, got)
want := expected(t, test, items)
if diff := tests.CheckCompletionOrder(want, got, false); diff != "" {
t.Errorf("%s: %s", src, diff)
@@ -66,9 +62,7 @@
opts.Matcher = source.CaseInsensitive
opts.UnimportedCompletion = false
})
- if !strings.Contains(string(src.URI()), "builtins") {
- got = tests.FilterBuiltins(got)
- }
+ got = tests.FilterBuiltins(src, got)
want := expected(t, test, items)
if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
@@ -81,9 +75,7 @@
opts.Matcher = source.Fuzzy
opts.UnimportedCompletion = false
})
- if !strings.Contains(string(src.URI()), "builtins") {
- got = tests.FilterBuiltins(got)
- }
+ got = tests.FilterBuiltins(src, got)
want := expected(t, test, items)
if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
@@ -95,9 +87,7 @@
opts.Matcher = source.CaseSensitive
opts.UnimportedCompletion = false
})
- if !strings.Contains(string(src.URI()), "builtins") {
- got = tests.FilterBuiltins(got)
- }
+ got = tests.FilterBuiltins(src, got)
want := expected(t, test, items)
if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
@@ -147,7 +137,7 @@
list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(src.URI()),
+ URI: protocol.URIFromSpanURI(src.URI()),
},
Position: protocol.Position{
Line: float64(src.Start().Line() - 1),
diff --git a/internal/lsp/debug/info.go b/internal/lsp/debug/info.go
index 84f365f..b3cecb5 100644
--- a/internal/lsp/debug/info.go
+++ b/internal/lsp/debug/info.go
@@ -23,14 +23,14 @@
// Version is a manually-updated mechanism for tracking versions.
var Version = "v0.3.2"
-// PrintServerInfo writes HTML debug info to w for the Instance s.
-func PrintServerInfo(w io.Writer, s Instance) {
+// PrintServerInfo writes HTML debug info to w for the Instance.
+func (i *Instance) PrintServerInfo(w io.Writer) {
section(w, HTML, "Server Instance", func() {
- fmt.Fprintf(w, "Start time: %v\n", s.StartTime())
- fmt.Fprintf(w, "LogFile: %s\n", s.Logfile())
- fmt.Fprintf(w, "Working directory: %s\n", s.Workdir())
- fmt.Fprintf(w, "Address: %s\n", s.Address())
- fmt.Fprintf(w, "Debug address: %s\n", s.Debug())
+ fmt.Fprintf(w, "Start time: %v\n", i.StartTime)
+ fmt.Fprintf(w, "LogFile: %s\n", i.Logfile)
+ fmt.Fprintf(w, "Working directory: %s\n", i.Workdir)
+ fmt.Fprintf(w, "Address: %s\n", i.ServerAddress)
+ fmt.Fprintf(w, "Debug address: %s\n", i.DebugAddress)
})
PrintVersionInfo(w, true, HTML)
}
diff --git a/internal/lsp/debug/rpc.go b/internal/lsp/debug/rpc.go
index fe31427..39574c1 100644
--- a/internal/lsp/debug/rpc.go
+++ b/internal/lsp/debug/rpc.go
@@ -17,7 +17,7 @@
"golang.org/x/tools/internal/telemetry/metric"
)
-var rpcTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var rpcTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}RPC Information{{end}}
{{define "body"}}
<H2>Inbound</H2>
@@ -90,11 +90,6 @@
Count int64
}
-func (r *rpcs) StartSpan(ctx context.Context, span *telemetry.Span) {}
-func (r *rpcs) FinishSpan(ctx context.Context, span *telemetry.Span) {}
-func (r *rpcs) Log(ctx context.Context, event telemetry.Event) {}
-func (r *rpcs) Flush() {}
-
func (r *rpcs) Metric(ctx context.Context, data telemetry.MetricData) {
for i, group := range data.Groups() {
set := &r.Inbound
diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go
index 40058ea..c1fb380 100644
--- a/internal/lsp/debug/serve.go
+++ b/internal/lsp/debug/serve.go
@@ -7,40 +7,176 @@
import (
"bytes"
"context"
+ "fmt"
"go/token"
"html/template"
+ "io"
stdlog "log"
"net"
"net/http"
"net/http/pprof"
_ "net/http/pprof" // pull in the standard pprof handlers
+ "os"
"path"
+ "path/filepath"
+ "reflect"
"runtime"
+ rpprof "runtime/pprof"
"strconv"
"strings"
"sync"
"time"
+ "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/telemetry"
"golang.org/x/tools/internal/telemetry/export"
+ "golang.org/x/tools/internal/telemetry/export/ocagent"
"golang.org/x/tools/internal/telemetry/export/prometheus"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/tag"
)
-type Instance interface {
- Logfile() string
- StartTime() time.Time
- Address() string
- Debug() string
- Workdir() string
+// An Instance holds all debug information associated with a gopls instance.
+type Instance struct {
+ Logfile string
+ StartTime time.Time
+ ServerAddress string
+ DebugAddress string
+ ListenedDebugAddress string
+ Workdir string
+ OCAgentConfig string
+
+ LogWriter io.Writer
+
+ ocagent export.Exporter
+ prometheus *prometheus.Exporter
+ rpcs *rpcs
+ traces *traces
+ State *State
}
+// State holds debugging information related to the server state.
+type State struct {
+ mu sync.Mutex
+ caches objset
+ sessions objset
+ views objset
+ clients objset
+ servers objset
+}
+
+type ider interface {
+ ID() string
+}
+
+type objset struct {
+ objs []ider
+}
+
+func (s *objset) add(elem ider) {
+ s.objs = append(s.objs, elem)
+}
+
+func (s *objset) drop(elem ider) {
+ var newobjs []ider
+ for _, obj := range s.objs {
+ if obj.ID() != elem.ID() {
+ newobjs = append(newobjs, obj)
+ }
+ }
+ s.objs = newobjs
+}
+
+func (s *objset) find(id string) ider {
+ for _, e := range s.objs {
+ if e.ID() == id {
+ return e
+ }
+ }
+ return nil
+}
+
+// Caches returns the set of Cache objects currently being served.
+func (st *State) Caches() []Cache {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ caches := make([]Cache, len(st.caches.objs))
+ for i, c := range st.caches.objs {
+ caches[i] = c.(Cache)
+ }
+ return caches
+}
+
+// Sessions returns the set of Session objects currently being served.
+func (st *State) Sessions() []Session {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ sessions := make([]Session, len(st.sessions.objs))
+ for i, s := range st.sessions.objs {
+ sessions[i] = s.(Session)
+ }
+ return sessions
+}
+
+// Views returns the set of View objects currently being served.
+func (st *State) Views() []View {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ views := make([]View, len(st.views.objs))
+ for i, v := range st.views.objs {
+ views[i] = v.(View)
+ }
+ return views
+}
+
+// Clients returns the set of Clients currently being served.
+func (st *State) Clients() []Client {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ clients := make([]Client, len(st.clients.objs))
+ for i, c := range st.clients.objs {
+ clients[i] = c.(Client)
+ }
+ return clients
+}
+
+// Servers returns the set of Servers the instance is currently connected to.
+func (st *State) Servers() []Server {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ servers := make([]Server, len(st.servers.objs))
+ for i, s := range st.servers.objs {
+ servers[i] = s.(Server)
+ }
+ return servers
+}
+
+// A Client is an incoming connection from a remote client.
+type Client interface {
+ ID() string
+ Session() Session
+ DebugAddress() string
+ Logfile() string
+ ServerID() string
+}
+
+// A Server is an outgoing connection to a remote LSP server.
+type Server interface {
+ ID() string
+ DebugAddress() string
+ Logfile() string
+ ClientID() string
+}
+
+// A Cache is an in-memory cache.
type Cache interface {
ID() string
FileSet() *token.FileSet
+ MemStats() map[reflect.Type]int
}
+// A Session is an LSP serving session.
type Session interface {
ID() string
Cache() Cache
@@ -48,6 +184,7 @@
File(hash string) *File
}
+// A View is a root directory within a Session.
type View interface {
ID() string
Name() string
@@ -55,6 +192,7 @@
Session() Session
}
+// A File is is a file within a session.
type File struct {
Session Session
URI span.URI
@@ -63,55 +201,91 @@
Hash string
}
-var (
- mu sync.Mutex
- data = struct {
- Caches []Cache
- Sessions []Session
- Views []View
- }{}
-)
-
-// AddCache adds a cache to the set being served
-func AddCache(cache Cache) {
- mu.Lock()
- defer mu.Unlock()
- data.Caches = append(data.Caches, cache)
+// AddCache adds a cache to the set being served.
+func (st *State) AddCache(cache Cache) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.caches.add(cache)
}
-// DropCache drops a cache from the set being served
-func DropCache(cache Cache) {
- mu.Lock()
- defer mu.Unlock()
- //find and remove the cache
- if i, _ := findCache(cache.ID()); i >= 0 {
- copy(data.Caches[i:], data.Caches[i+1:])
- data.Caches[len(data.Caches)-1] = nil
- data.Caches = data.Caches[:len(data.Caches)-1]
- }
+// DropCache drops a cache from the set being served.
+func (st *State) DropCache(cache Cache) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.caches.drop(cache)
}
-func findCache(id string) (int, Cache) {
- for i, c := range data.Caches {
- if c.ID() == id {
- return i, c
- }
- }
- return -1, nil
+// AddSession adds a session to the set being served.
+func (st *State) AddSession(session Session) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.sessions.add(session)
}
-func getCache(r *http.Request) interface{} {
- mu.Lock()
- defer mu.Unlock()
+// DropSession drops a session from the set being served.
+func (st *State) DropSession(session Session) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.sessions.drop(session)
+}
+
+// AddView adds a view to the set being served.
+func (st *State) AddView(view View) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.views.add(view)
+}
+
+// DropView drops a view from the set being served.
+func (st *State) DropView(view View) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.views.drop(view)
+}
+
+// AddClient adds a client to the set being served.
+func (st *State) AddClient(client Client) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.clients.add(client)
+}
+
+// DropClient adds a client to the set being served.
+func (st *State) DropClient(client Client) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.clients.drop(client)
+}
+
+// AddServer adds a server to the set being queried. In practice, there should
+// be at most one remote server.
+func (st *State) AddServer(server Server) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.servers.add(server)
+}
+
+// DropServer drops a server to the set being queried.
+func (st *State) DropServer(server Server) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ st.servers.drop(server)
+}
+
+func (i *Instance) getCache(r *http.Request) interface{} {
+ i.State.mu.Lock()
+ defer i.State.mu.Unlock()
id := path.Base(r.URL.Path)
result := struct {
Cache
Sessions []Session
- }{}
- _, result.Cache = findCache(id)
+ }{
+ Cache: i.State.caches.find(id).(Cache),
+ }
// now find all the views that belong to this session
- for _, v := range data.Sessions {
+ for _, vd := range i.State.sessions.objs {
+ v := vd.(Session)
if v.Cache().ID() == id {
result.Sessions = append(result.Sessions, v)
}
@@ -119,27 +293,19 @@
return result
}
-func findSession(id string) Session {
- for _, c := range data.Sessions {
- if c.ID() == id {
- return c
- }
- }
- return nil
-}
-
-func getSession(r *http.Request) interface{} {
- mu.Lock()
- defer mu.Unlock()
+func (i *Instance) getSession(r *http.Request) interface{} {
+ i.State.mu.Lock()
+ defer i.State.mu.Unlock()
id := path.Base(r.URL.Path)
result := struct {
Session
Views []View
}{
- Session: findSession(id),
+ Session: i.State.sessions.find(id).(Session),
}
// now find all the views that belong to this session
- for _, v := range data.Views {
+ for _, vd := range i.State.views.objs {
+ v := vd.(View)
if v.Session().ID() == id {
result.Views = append(result.Views, v)
}
@@ -147,37 +313,39 @@
return result
}
-func findView(id string) View {
- for _, c := range data.Views {
- if c.ID() == id {
- return c
- }
- }
- return nil
-}
-
-func getView(r *http.Request) interface{} {
- mu.Lock()
- defer mu.Unlock()
+func (i Instance) getClient(r *http.Request) interface{} {
+ i.State.mu.Lock()
+ defer i.State.mu.Unlock()
id := path.Base(r.URL.Path)
- return findView(id)
+ return i.State.clients.find(id).(Client)
}
-func getFile(r *http.Request) interface{} {
- mu.Lock()
- defer mu.Unlock()
+func (i Instance) getServer(r *http.Request) interface{} {
+ i.State.mu.Lock()
+ defer i.State.mu.Unlock()
+ id := path.Base(r.URL.Path)
+ return i.State.servers.find(id).(Server)
+}
+
+func (i Instance) getView(r *http.Request) interface{} {
+ i.State.mu.Lock()
+ defer i.State.mu.Unlock()
+ id := path.Base(r.URL.Path)
+ return i.State.views.find(id).(View)
+}
+
+func (i *Instance) getFile(r *http.Request) interface{} {
+ i.State.mu.Lock()
+ defer i.State.mu.Unlock()
hash := path.Base(r.URL.Path)
sid := path.Base(path.Dir(r.URL.Path))
- session := findSession(sid)
- return session.File(hash)
+ return i.State.sessions.find(sid).(Session).File(hash)
}
-func getInfo(s Instance) dataFunc {
- return func(r *http.Request) interface{} {
- buf := &bytes.Buffer{}
- PrintServerInfo(buf, s)
- return template.HTML(buf.String())
- }
+func (i *Instance) getInfo(r *http.Request) interface{} {
+ buf := &bytes.Buffer{}
+ i.PrintServerInfo(buf)
+ return template.HTML(buf.String())
}
func getMemory(r *http.Request) interface{} {
@@ -186,75 +354,94 @@
return m
}
-// AddSession adds a session to the set being served
-func AddSession(session Session) {
- mu.Lock()
- defer mu.Unlock()
- data.Sessions = append(data.Sessions, session)
+// NewInstance creates debug instance ready for use using the supplied configuration.
+func NewInstance(workdir, agent string) *Instance {
+ i := &Instance{
+ StartTime: time.Now(),
+ Workdir: workdir,
+ OCAgentConfig: agent,
+ }
+ i.LogWriter = os.Stderr
+ ocConfig := ocagent.Discover()
+ //TODO: we should not need to adjust the discovered configuration
+ ocConfig.Address = i.OCAgentConfig
+ i.ocagent = ocagent.Connect(ocConfig)
+ i.prometheus = prometheus.New()
+ i.rpcs = &rpcs{}
+ i.traces = &traces{}
+ i.State = &State{}
+ export.SetExporter(i)
+ return i
}
-// DropSession drops a session from the set being served
-func DropSession(session Session) {
- mu.Lock()
- defer mu.Unlock()
- //find and remove the session
-}
-
-// AddView adds a view to the set being served
-func AddView(view View) {
- mu.Lock()
- defer mu.Unlock()
- data.Views = append(data.Views, view)
-}
-
-// DropView drops a view from the set being served
-func DropView(view View) {
- mu.Lock()
- defer mu.Unlock()
- //find and remove the view
+// SetLogFile sets the logfile for use with this instance.
+func (i *Instance) SetLogFile(logfile string) (func(), error) {
+ // TODO: probably a better solution for deferring closure to the caller would
+ // be for the debug instance to itself be closed, but this fixes the
+ // immediate bug of logs not being captured.
+ closeLog := func() {}
+ if logfile != "" {
+ if logfile == "auto" {
+ logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.log", os.Getpid()))
+ }
+ f, err := os.Create(logfile)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create log file: %v", err)
+ }
+ closeLog = func() {
+ defer f.Close()
+ }
+ stdlog.SetOutput(io.MultiWriter(os.Stderr, f))
+ i.LogWriter = f
+ }
+ i.Logfile = logfile
+ return closeLog, nil
}
// Serve starts and runs a debug server in the background.
// It also logs the port the server starts on, to allow for :0 auto assigned
// ports.
-func Serve(ctx context.Context, addr string, instance Instance) error {
- mu.Lock()
- defer mu.Unlock()
- if addr == "" {
+func (i *Instance) Serve(ctx context.Context) error {
+ if i.DebugAddress == "" {
return nil
}
- listener, err := net.Listen("tcp", addr)
+ listener, err := net.Listen("tcp", i.DebugAddress)
if err != nil {
return err
}
+ i.ListenedDebugAddress = listener.Addr().String()
port := listener.Addr().(*net.TCPAddr).Port
- if strings.HasSuffix(addr, ":0") {
+ if strings.HasSuffix(i.DebugAddress, ":0") {
stdlog.Printf("debug server listening on port %d", port)
}
log.Print(ctx, "Debug serving", tag.Of("Port", port))
- prometheus := prometheus.New()
- rpcs := &rpcs{}
- traces := &traces{}
- export.AddExporters(prometheus, rpcs, traces)
go func() {
mux := http.NewServeMux()
- mux.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data }))
- mux.HandleFunc("/debug/", Render(debugTmpl, nil))
+ mux.HandleFunc("/", render(mainTmpl, func(*http.Request) interface{} { return i }))
+ mux.HandleFunc("/debug/", render(debugTmpl, nil))
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
- mux.HandleFunc("/metrics/", prometheus.Serve)
- mux.HandleFunc("/rpc/", Render(rpcTmpl, rpcs.getData))
- mux.HandleFunc("/trace/", Render(traceTmpl, traces.getData))
- mux.HandleFunc("/cache/", Render(cacheTmpl, getCache))
- mux.HandleFunc("/session/", Render(sessionTmpl, getSession))
- mux.HandleFunc("/view/", Render(viewTmpl, getView))
- mux.HandleFunc("/file/", Render(fileTmpl, getFile))
- mux.HandleFunc("/info", Render(infoTmpl, getInfo(instance)))
- mux.HandleFunc("/memory", Render(memoryTmpl, getMemory))
+ if i.prometheus != nil {
+ mux.HandleFunc("/metrics/", i.prometheus.Serve)
+ }
+ if i.rpcs != nil {
+ mux.HandleFunc("/rpc/", render(rpcTmpl, i.rpcs.getData))
+ }
+ if i.traces != nil {
+ mux.HandleFunc("/trace/", render(traceTmpl, i.traces.getData))
+ }
+ mux.HandleFunc("/cache/", render(cacheTmpl, i.getCache))
+ mux.HandleFunc("/session/", render(sessionTmpl, i.getSession))
+ mux.HandleFunc("/view/", render(viewTmpl, i.getView))
+ mux.HandleFunc("/client/", render(clientTmpl, i.getClient))
+ mux.HandleFunc("/server/", render(serverTmpl, i.getServer))
+ mux.HandleFunc("/file/", render(fileTmpl, i.getFile))
+ mux.HandleFunc("/info", render(infoTmpl, i.getInfo))
+ mux.HandleFunc("/memory", render(memoryTmpl, getMemory))
if err := http.Serve(listener, mux); err != nil {
log.Error(ctx, "Debug server failed", err)
return
@@ -264,9 +451,104 @@
return nil
}
+// MonitorMemory starts recording memory statistics each second.
+func (i *Instance) MonitorMemory(ctx context.Context) {
+ tick := time.NewTicker(time.Second)
+ nextThresholdGiB := uint64(1)
+ go func() {
+ for {
+ <-tick.C
+ var mem runtime.MemStats
+ runtime.ReadMemStats(&mem)
+ if mem.HeapAlloc < nextThresholdGiB*1<<30 {
+ continue
+ }
+ i.writeMemoryDebug(nextThresholdGiB)
+ log.Print(ctx, fmt.Sprintf("Wrote memory usage debug info to %v", os.TempDir()))
+ nextThresholdGiB++
+ }
+ }()
+}
+
+func (i *Instance) writeMemoryDebug(threshold uint64) error {
+ fname := func(t string) string {
+ return fmt.Sprintf("gopls.%d-%dGiB-%s", os.Getpid(), threshold, t)
+ }
+
+ f, err := os.Create(filepath.Join(os.TempDir(), fname("heap.pb.gz")))
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ if err := rpprof.Lookup("heap").WriteTo(f, 0); err != nil {
+ return err
+ }
+
+ f, err = os.Create(filepath.Join(os.TempDir(), fname("goroutines.txt")))
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ if err := rpprof.Lookup("goroutine").WriteTo(f, 1); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (i *Instance) StartSpan(ctx context.Context, spn *telemetry.Span) {
+ if i.ocagent != nil {
+ i.ocagent.StartSpan(ctx, spn)
+ }
+ if i.traces != nil {
+ i.traces.StartSpan(ctx, spn)
+ }
+}
+
+func (i *Instance) FinishSpan(ctx context.Context, spn *telemetry.Span) {
+ if i.ocagent != nil {
+ i.ocagent.FinishSpan(ctx, spn)
+ }
+ if i.traces != nil {
+ i.traces.FinishSpan(ctx, spn)
+ }
+}
+
+//TODO: remove this hack
+// capture stderr at startup because it gets modified in a way that this
+// logger should not respect
+var stderr = os.Stderr
+
+func (i *Instance) Log(ctx context.Context, event telemetry.Event) {
+ if event.Error != nil {
+ fmt.Fprintf(stderr, "%v\n", event)
+ }
+ protocol.LogEvent(ctx, event)
+ if i.ocagent != nil {
+ i.ocagent.Log(ctx, event)
+ }
+}
+
+func (i *Instance) Metric(ctx context.Context, data telemetry.MetricData) {
+ if i.ocagent != nil {
+ i.ocagent.Metric(ctx, data)
+ }
+ if i.traces != nil {
+ i.prometheus.Metric(ctx, data)
+ }
+ if i.rpcs != nil {
+ i.rpcs.Metric(ctx, data)
+ }
+}
+
+func (i *Instance) Flush() {
+ if i.ocagent != nil {
+ i.ocagent.Flush()
+ }
+}
+
type dataFunc func(*http.Request) interface{}
-func Render(tmpl *template.Template, fun dataFunc) func(http.ResponseWriter, *http.Request) {
+func render(tmpl *template.Template, fun dataFunc) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var data interface{}
if fun != nil {
@@ -274,6 +556,7 @@
}
if err := tmpl.Execute(w, data); err != nil {
log.Error(context.Background(), "", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
@@ -294,7 +577,7 @@
return commas(strconv.FormatUint(uint64(v), 10))
}
-var BaseTemplate = template.Must(template.New("").Parse(`
+var baseTemplate = template.Must(template.New("").Parse(`
<html>
<head>
<title>{{template "title" .}}</title>
@@ -329,34 +612,60 @@
</html>
{{define "cachelink"}}<a href="/cache/{{.}}">Cache {{.}}</a>{{end}}
+{{define "clientlink"}}<a href="/client/{{.}}">Client {{.}}</a>{{end}}
+{{define "serverlink"}}<a href="/server/{{.}}">Server {{.}}</a>{{end}}
{{define "sessionlink"}}<a href="/session/{{.}}">Session {{.}}</a>{{end}}
{{define "viewlink"}}<a href="/view/{{.}}">View {{.}}</a>{{end}}
{{define "filelink"}}<a href="/file/{{.Session.ID}}/{{.Hash}}">{{.URI}}</a>{{end}}
`)).Funcs(template.FuncMap{
"fuint64": fuint64,
"fuint32": fuint32,
+ "localAddress": func(s string) string {
+ // Try to translate loopback addresses to localhost, both for cosmetics and
+ // because unspecified ipv6 addresses can break links on Windows.
+ //
+ // TODO(rfindley): In the future, it would be better not to assume the
+ // server is running on localhost, and instead construct this address using
+ // the remote host.
+ host, port, err := net.SplitHostPort(s)
+ if err != nil {
+ return s
+ }
+ ip := net.ParseIP(host)
+ if ip == nil {
+ return s
+ }
+ if ip.IsLoopback() || ip.IsUnspecified() {
+ return "localhost:" + port
+ }
+ return s
+ },
})
-var mainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var mainTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}GoPls server information{{end}}
{{define "body"}}
<h2>Caches</h2>
-<ul>{{range .Caches}}<li>{{template "cachelink" .ID}}</li>{{end}}</ul>
+<ul>{{range .State.Caches}}<li>{{template "cachelink" .ID}}</li>{{end}}</ul>
<h2>Sessions</h2>
-<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}</li>{{end}}</ul>
+<ul>{{range .State.Sessions}}<li>{{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}</li>{{end}}</ul>
<h2>Views</h2>
-<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}</li>{{end}}</ul>
+<ul>{{range .State.Views}}<li>{{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}</li>{{end}}</ul>
+<h2>Clients</h2>
+<ul>{{range .State.Clients}}<li>{{template "clientlink" .ID}}</li>{{end}}</ul>
+<h2>Servers</h2>
+<ul>{{range .State.Servers}}<li>{{template "serverlink" .ID}}</li>{{end}}</ul>
{{end}}
`))
-var infoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var infoTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}GoPls version information{{end}}
{{define "body"}}
{{.}}
{{end}}
`))
-var memoryTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var memoryTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}GoPls memory usage{{end}}
{{define "head"}}<meta http-equiv="refresh" content="5">{{end}}
{{define "body"}}
@@ -386,22 +695,43 @@
{{end}}
`))
-var debugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var debugTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}GoPls Debug pages{{end}}
{{define "body"}}
<a href="/debug/pprof">Profiling</a>
{{end}}
`))
-var cacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var cacheTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}Cache {{.ID}}{{end}}
{{define "body"}}
<h2>Sessions</h2>
<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}}</li>{{end}}</ul>
+<h2>memoize.Store entries</h2>
+<ul>{{range $k,$v := .MemStats}}<li>{{$k}} - {{$v}}</li>{{end}}</ul>
{{end}}
`))
-var sessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var clientTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+{{define "title"}}Client {{.ID}}{{end}}
+{{define "body"}}
+Using session: <b>{{template "sessionlink" .Session.ID}}</b><br>
+Debug this client at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br>
+Logfile: {{.Logfile}}<br>
+Gopls Path: {{.GoplsPath}}<br>
+{{end}}
+`))
+
+var serverTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
+{{define "title"}}Server {{.ID}}{{end}}
+{{define "body"}}
+Debug this server at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br>
+Logfile: {{.Logfile}}<br>
+Gopls Path: {{.GoplsPath}}<br>
+{{end}}
+`))
+
+var sessionTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}Session {{.ID}}{{end}}
{{define "body"}}
From: <b>{{template "cachelink" .Cache.ID}}</b><br>
@@ -412,7 +742,7 @@
{{end}}
`))
-var viewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var viewTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}View {{.ID}}{{end}}
{{define "body"}}
Name: <b>{{.Name}}</b><br>
@@ -423,7 +753,7 @@
{{end}}
`))
-var fileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var fileTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}File {{.Hash}}{{end}}
{{define "body"}}
From: <b>{{template "sessionlink" .Session.ID}}</b><br>
diff --git a/internal/lsp/debug/serve_test.go b/internal/lsp/debug/serve_test.go
new file mode 100644
index 0000000..ff981e9
--- /dev/null
+++ b/internal/lsp/debug/serve_test.go
@@ -0,0 +1,54 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package debug
+
+import "testing"
+
+type fakeCache struct {
+ Cache
+
+ id string
+}
+
+func (c fakeCache) ID() string {
+ return c.id
+}
+
+func TestState(t *testing.T) {
+ c1 := fakeCache{id: "1"}
+ c2 := fakeCache{id: "2"}
+ c3 := fakeCache{id: "3"}
+
+ var s State
+ s.AddCache(c1)
+ s.AddCache(c2)
+ s.AddCache(c3)
+
+ compareCaches := func(desc string, want []fakeCache) {
+ t.Run(desc, func(t *testing.T) {
+ caches := s.Caches()
+ if gotLen, wantLen := len(caches), len(want); gotLen != wantLen {
+ t.Fatalf("len(Caches) = %d, want %d", gotLen, wantLen)
+ }
+ for i, got := range caches {
+ if got != want[i] {
+ t.Errorf("Caches[%d] = %v, want %v", i, got, want[i])
+ }
+ }
+ })
+ }
+
+ compareCaches("initial load", []fakeCache{c1, c2, c3})
+ s.DropCache(c2)
+ compareCaches("dropped cache 2", []fakeCache{c1, c3})
+ s.DropCache(c2)
+ compareCaches("duplicate drop", []fakeCache{c1, c3})
+ s.AddCache(c2)
+ compareCaches("re-add cache 2", []fakeCache{c1, c3, c2})
+ s.DropCache(c1)
+ s.DropCache(c2)
+ s.DropCache(c3)
+ compareCaches("drop all", []fakeCache{})
+}
diff --git a/internal/lsp/debug/trace.go b/internal/lsp/debug/trace.go
index 4fd3de4..f71b5a6 100644
--- a/internal/lsp/debug/trace.go
+++ b/internal/lsp/debug/trace.go
@@ -17,7 +17,7 @@
"golang.org/x/tools/internal/telemetry"
)
-var traceTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+var traceTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(`
{{define "title"}}Trace Information{{end}}
{{define "body"}}
{{range .Traces}}<a href="/trace/{{.Name}}">{{.Name}}</a> last: {{.Last.Duration}}, longest: {{.Longest.Duration}}<br>{{end}}
@@ -130,12 +130,6 @@
}
}
-func (t *traces) Log(ctx context.Context, event telemetry.Event) {}
-
-func (t *traces) Metric(ctx context.Context, data telemetry.MetricData) {}
-
-func (t *traces) Flush() {}
-
func (t *traces) getData(req *http.Request) interface{} {
if len(t.sets) == 0 {
return nil
diff --git a/internal/lsp/definition.go b/internal/lsp/definition.go
index e8b8b54..440a481 100644
--- a/internal/lsp/definition.go
+++ b/internal/lsp/definition.go
@@ -9,24 +9,14 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
)
func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
- if fh.Identity().Kind != source.Go {
- return nil, nil
- }
- ident, err := source.Identifier(ctx, snapshot, fh, params.Position, source.WidestPackageHandle)
+ ident, err := source.Identifier(ctx, snapshot, fh, params.Position)
if err != nil {
return nil, err
}
@@ -36,27 +26,18 @@
}
return []protocol.Location{
{
- URI: protocol.NewURI(ident.Declaration.URI()),
+ URI: protocol.URIFromSpanURI(ident.Declaration.URI()),
Range: decRange,
},
}, nil
}
func (s *Server) typeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
- if fh.Identity().Kind != source.Go {
- return nil, nil
- }
- ident, err := source.Identifier(ctx, snapshot, fh, params.Position, source.WidestPackageHandle)
+ ident, err := source.Identifier(ctx, snapshot, fh, params.Position)
if err != nil {
return nil, err
}
@@ -66,7 +47,7 @@
}
return []protocol.Location{
{
- URI: protocol.NewURI(ident.Type.URI()),
+ URI: protocol.URIFromSpanURI(ident.Type.URI()),
Range: identRange,
},
}, nil
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 2cfa8ea..0319c08 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -6,7 +6,9 @@
import (
"context"
+ "fmt"
"strings"
+ "sync"
"golang.org/x/tools/internal/lsp/mod"
"golang.org/x/tools/internal/lsp/protocol"
@@ -17,80 +19,132 @@
"golang.org/x/tools/internal/xcontext"
)
+type diagnosticKey struct {
+ id source.FileIdentity
+ withAnalysis bool
+}
+
func (s *Server) diagnoseDetached(snapshot source.Snapshot) {
ctx := snapshot.View().BackgroundContext()
ctx = xcontext.Detach(ctx)
- s.diagnose(ctx, snapshot)
+ reports := s.diagnose(ctx, snapshot, false)
+ s.publishReports(ctx, snapshot, reports)
}
func (s *Server) diagnoseSnapshot(snapshot source.Snapshot) {
ctx := snapshot.View().BackgroundContext()
- s.diagnose(ctx, snapshot)
+ reports := s.diagnose(ctx, snapshot, false)
+ s.publishReports(ctx, snapshot, reports)
}
// diagnose is a helper function for running diagnostics with a given context.
// Do not call it directly.
-func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot) {
+func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysAnalyze bool) map[diagnosticKey][]source.Diagnostic {
ctx, done := trace.StartSpan(ctx, "lsp:background-worker")
defer done()
- // Diagnose all of the packages in the workspace.
- go func() {
- wsPackages, err := snapshot.WorkspacePackages(ctx)
- if ctx.Err() != nil {
- return
- }
- if err != nil {
- log.Error(ctx, "diagnose: no workspace packages", err, telemetry.Snapshot.Of(snapshot.ID()), telemetry.Directory.Of(snapshot.View().Folder))
- return
- }
- for _, ph := range wsPackages {
- go func(ph source.PackageHandle) {
- // Only run analyses for packages with open files.
- var withAnalyses bool
- for _, fh := range ph.CompiledGoFiles() {
- if s.session.IsOpen(fh.File().Identity().URI) {
- withAnalyses = true
- }
- }
- reports, warn, err := source.Diagnostics(ctx, snapshot, ph, withAnalyses)
- // Check if might want to warn the user about their build configuration.
- if warn && !snapshot.View().ValidBuildConfiguration() {
- s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
- Type: protocol.Warning,
- // TODO(rstambler): We should really be able to point to a link on the website.
- Message: `You are neither in a module nor in your GOPATH. Please see https://github.com/golang/go/wiki/Modules for information on how to set up your Go project.`,
- })
- }
- if ctx.Err() != nil {
- return
- }
- if err != nil {
- log.Error(ctx, "diagnose: could not generate diagnostics for package", err, telemetry.Snapshot.Of(snapshot.ID()), telemetry.Package.Of(ph.ID()))
- return
- }
- s.publishReports(ctx, snapshot, reports, withAnalyses)
- }(ph)
- }
- }()
+ // Wait for a free diagnostics slot.
+ select {
+ case <-ctx.Done():
+ return nil
+ case s.diagnosticsSema <- struct{}{}:
+ }
+ defer func() { <-s.diagnosticsSema }()
+
+ allReports := make(map[diagnosticKey][]source.Diagnostic)
+ var reportsMu sync.Mutex
+ var wg sync.WaitGroup
// Diagnose the go.mod file.
- go func() {
- reports, err := mod.Diagnostics(ctx, snapshot)
- if ctx.Err() != nil {
- return
+ reports, missingModules, err := mod.Diagnostics(ctx, snapshot)
+ if ctx.Err() != nil {
+ return nil
+ }
+ if err != nil {
+ log.Error(ctx, "diagnose: could not generate diagnostics for go.mod file", err)
+ }
+ // Ensure that the reports returned from mod.Diagnostics are only related to the
+ // go.mod file for the module.
+ if len(reports) > 1 {
+ panic("unexpected reports from mod.Diagnostics")
+ }
+ modURI, _ := snapshot.View().ModFiles()
+ for id, diags := range reports {
+ if id.URI != modURI {
+ panic("unexpected reports from mod.Diagnostics")
}
- if err != nil {
- log.Error(ctx, "diagnose: could not generate diagnostics for go.mod file", err)
- return
+ key := diagnosticKey{
+ id: id,
}
- s.publishReports(ctx, snapshot, reports, false)
- }()
+ allReports[key] = diags
+ }
+
+ // Diagnose all of the packages in the workspace.
+ wsPackages, err := snapshot.WorkspacePackages(ctx)
+ if ctx.Err() != nil {
+ return nil
+ }
+ if err != nil {
+ // If we encounter a genuine error when getting workspace packages,
+ // notify the user.
+ s.showedInitialErrorMu.Lock()
+ if !s.showedInitialError {
+ err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+ Type: protocol.Error,
+ Message: fmt.Sprintf("Your workspace is misconfigured: %s. Please see https://github.com/golang/tools/blob/master/gopls/doc/troubleshooting.md for more information or file an issue (https://github.com/golang/go/issues/new) if you believe this is a mistake.", err.Error()),
+ })
+ s.showedInitialError = err == nil
+ }
+ s.showedInitialErrorMu.Unlock()
+
+ log.Error(ctx, "diagnose: no workspace packages", err, telemetry.Snapshot.Of(snapshot.ID()), telemetry.Directory.Of(snapshot.View().Folder))
+ return nil
+ }
+ for _, ph := range wsPackages {
+ wg.Add(1)
+ go func(ph source.PackageHandle) {
+ defer wg.Done()
+ // Only run analyses for packages with open files.
+ withAnalyses := alwaysAnalyze
+ for _, fh := range ph.CompiledGoFiles() {
+ if snapshot.IsOpen(fh.File().Identity().URI) {
+ withAnalyses = true
+ }
+ }
+ reports, warn, err := source.Diagnostics(ctx, snapshot, ph, missingModules, withAnalyses)
+ // Check if might want to warn the user about their build configuration.
+ if warn && !snapshot.View().ValidBuildConfiguration() {
+ s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+ Type: protocol.Warning,
+ // TODO(rstambler): We should really be able to point to a link on the website.
+ Message: `You are neither in a module nor in your GOPATH. Please see https://github.com/golang/go/wiki/Modules for information on how to set up your Go project.`,
+ })
+ }
+ if ctx.Err() != nil {
+ return
+ }
+ if err != nil {
+ log.Error(ctx, "diagnose: could not generate diagnostics for package", err, telemetry.Snapshot.Of(snapshot.ID()), telemetry.Package.Of(ph.ID()))
+ return
+ }
+ reportsMu.Lock()
+ for id, diags := range reports {
+ key := diagnosticKey{
+ id: id,
+ withAnalysis: withAnalyses,
+ }
+ allReports[key] = diags
+ }
+ reportsMu.Unlock()
+ }(ph)
+ }
+ wg.Wait()
+ return allReports
}
-func (s *Server) publishReports(ctx context.Context, snapshot source.Snapshot, reports map[source.FileIdentity][]source.Diagnostic, withAnalysis bool) {
+func (s *Server) publishReports(ctx context.Context, snapshot source.Snapshot, reports map[diagnosticKey][]source.Diagnostic) {
// Check for context cancellation before publishing diagnostics.
if ctx.Err() != nil {
return
@@ -99,7 +153,7 @@
s.deliveredMu.Lock()
defer s.deliveredMu.Unlock()
- for fileID, diagnostics := range reports {
+ for key, diagnostics := range reports {
// Don't deliver diagnostics if the context has already been canceled.
if ctx.Err() != nil {
break
@@ -108,15 +162,15 @@
// Pre-sort diagnostics to avoid extra work when we compare them.
source.SortDiagnostics(diagnostics)
toSend := sentDiagnostics{
- version: fileID.Version,
- identifier: fileID.Identifier,
+ version: key.id.Version,
+ identifier: key.id.Identifier,
sorted: diagnostics,
- withAnalysis: withAnalysis,
+ withAnalysis: key.withAnalysis,
snapshotID: snapshot.ID(),
}
// We use the zero values if this is an unknown file.
- delivered := s.delivered[fileID.URI]
+ delivered := s.delivered[key.id.URI]
// Snapshot IDs are always increasing, so we use them instead of file
// versions to create the correct order for diagnostics.
@@ -131,7 +185,7 @@
// Check if we should reuse the cached diagnostics.
if equalDiagnostics(delivered.sorted, diagnostics) {
// Make sure to update the delivered map.
- s.delivered[fileID.URI] = toSend
+ s.delivered[key.id.URI] = toSend
continue
}
@@ -145,8 +199,8 @@
if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
Diagnostics: toProtocolDiagnostics(diagnostics),
- URI: protocol.NewURI(fileID.URI),
- Version: fileID.Version,
+ URI: protocol.URIFromSpanURI(key.id.URI),
+ Version: key.id.Version,
}); err != nil {
if ctx.Err() == nil {
log.Error(ctx, "publishReports: failed to deliver diagnostic", err, telemetry.File)
@@ -154,7 +208,7 @@
continue
}
// Update the delivered map.
- s.delivered[fileID.URI] = toSend
+ s.delivered[key.id.URI] = toSend
}
}
@@ -179,7 +233,7 @@
for _, rel := range diag.Related {
related = append(related, protocol.DiagnosticRelatedInformation{
Location: protocol.Location{
- URI: protocol.NewURI(rel.URI),
+ URI: protocol.URIFromSpanURI(rel.URI),
Range: rel.Range,
},
Message: rel.Message,
diff --git a/internal/lsp/diff/difftest/difftest.go b/internal/lsp/diff/difftest/difftest.go
index 297515f..513a925 100644
--- a/internal/lsp/diff/difftest/difftest.go
+++ b/internal/lsp/diff/difftest/difftest.go
@@ -222,7 +222,7 @@
for _, test := range TestCases {
t.Run(test.Name, func(t *testing.T) {
t.Helper()
- edits := compute(span.FileURI("/"+test.Name), test.In, test.Out)
+ edits := compute(span.URIFromPath("/"+test.Name), test.In, test.Out)
got := diff.ApplyEdits(test.In, edits)
unified := fmt.Sprint(diff.ToUnified(FileA, FileB, test.In, edits))
if got != test.Out {
diff --git a/internal/lsp/fake/client.go b/internal/lsp/fake/client.go
new file mode 100644
index 0000000..b4ff1f8
--- /dev/null
+++ b/internal/lsp/fake/client.go
@@ -0,0 +1,108 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fake
+
+import (
+ "context"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+)
+
+// Client is an adapter that converts a *Client into an LSP Client.
+type Client struct {
+ *Editor
+
+ // Hooks for testing. Add additional hooks here as needed for testing.
+ onLogMessage func(context.Context, *protocol.LogMessageParams) error
+ onDiagnostics func(context.Context, *protocol.PublishDiagnosticsParams) error
+}
+
+// OnLogMessage sets the hook to run when the editor receives a log message.
+func (c *Client) OnLogMessage(hook func(context.Context, *protocol.LogMessageParams) error) {
+ c.mu.Lock()
+ c.onLogMessage = hook
+ c.mu.Unlock()
+}
+
+// OnDiagnostics sets the hook to run when the editor receives diagnostics
+// published from the language server.
+func (c *Client) OnDiagnostics(hook func(context.Context, *protocol.PublishDiagnosticsParams) error) {
+ c.mu.Lock()
+ c.onDiagnostics = hook
+ c.mu.Unlock()
+}
+
+func (c *Client) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error {
+ c.mu.Lock()
+ c.lastMessage = params
+ c.mu.Unlock()
+ return nil
+}
+
+func (c *Client) ShowMessageRequest(ctx context.Context, params *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
+ return nil, nil
+}
+
+func (c *Client) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error {
+ c.mu.Lock()
+ c.logs = append(c.logs, params)
+ onLogMessage := c.onLogMessage
+ c.mu.Unlock()
+ if onLogMessage != nil {
+ return onLogMessage(ctx, params)
+ }
+ return nil
+}
+
+func (c *Client) Event(ctx context.Context, event *interface{}) error {
+ c.mu.Lock()
+ c.events = append(c.events, event)
+ c.mu.Unlock()
+ return nil
+}
+
+func (c *Client) PublishDiagnostics(ctx context.Context, params *protocol.PublishDiagnosticsParams) error {
+ c.mu.Lock()
+ c.diagnostics = params
+ onPublishDiagnostics := c.onDiagnostics
+ c.mu.Unlock()
+ if onPublishDiagnostics != nil {
+ return onPublishDiagnostics(ctx, params)
+ }
+ return nil
+}
+
+func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, error) {
+ return []protocol.WorkspaceFolder{}, nil
+}
+
+func (c *Client) Configuration(context.Context, *protocol.ParamConfiguration) ([]interface{}, error) {
+ return []interface{}{c.configuration()}, nil
+}
+
+func (c *Client) RegisterCapability(context.Context, *protocol.RegistrationParams) error {
+ return nil
+}
+
+func (c *Client) UnregisterCapability(context.Context, *protocol.UnregistrationParams) error {
+ return nil
+}
+
+// ApplyEdit applies edits sent from the server. Note that as of writing gopls
+// doesn't use this feature, so it is untested.
+func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResponse, error) {
+ if len(params.Edit.Changes) != 0 {
+ return &protocol.ApplyWorkspaceEditResponse{FailureReason: "Edit.Changes is unsupported"}, nil
+ }
+ for _, change := range params.Edit.DocumentChanges {
+ path := c.ws.URIToPath(change.TextDocument.URI)
+ var edits []Edit
+ for _, lspEdit := range change.Edits {
+ edits = append(edits, fromProtocolTextEdit(lspEdit))
+ }
+ c.EditBuffer(ctx, path, edits)
+ }
+ return &protocol.ApplyWorkspaceEditResponse{Applied: true}, nil
+}
diff --git a/internal/lsp/fake/doc.go b/internal/lsp/fake/doc.go
new file mode 100644
index 0000000..69e4a49
--- /dev/null
+++ b/internal/lsp/fake/doc.go
@@ -0,0 +1,19 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package fake provides fake implementations of a text editor, LSP client
+// plugin, and workspace for use in tests.
+//
+// The Editor type provides a high level API for text editor operations
+// (open/modify/save/close a buffer, jump to definition, etc.), and the Client
+// type exposes an LSP client for the editor that can be connected to a
+// language server. By default, the Editor and Client should be compliant with
+// the LSP spec: their intended use is to verify server compliance with the
+// spec in a variety of environment. Possible future enhancements of these
+// types may allow them to misbehave in configurable ways, but that is not
+// their primary use.
+//
+// The Workspace type provides a facility for executing tests in a clean
+// workspace and GOPATH.
+package fake
diff --git a/internal/lsp/fake/edit.go b/internal/lsp/fake/edit.go
new file mode 100644
index 0000000..1eec597
--- /dev/null
+++ b/internal/lsp/fake/edit.go
@@ -0,0 +1,99 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fake
+
+import (
+ "fmt"
+ "strings"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+)
+
+// Pos represents a 0-indexed position in a text buffer.
+type Pos struct {
+ Line, Column int
+}
+
+func (p Pos) toProtocolPosition() protocol.Position {
+ return protocol.Position{
+ Line: float64(p.Line),
+ Character: float64(p.Column),
+ }
+}
+
+func fromProtocolPosition(pos protocol.Position) Pos {
+ return Pos{
+ Line: int(pos.Line),
+ Column: int(pos.Character),
+ }
+}
+
+// Edit represents a single (contiguous) buffer edit.
+type Edit struct {
+ Start, End Pos
+ Text string
+}
+
+// NewEdit creates an edit replacing all content between
+// (startLine, startColumn) and (endLine, endColumn) with text.
+func NewEdit(startLine, startColumn, endLine, endColumn int, text string) Edit {
+ return Edit{
+ Start: Pos{Line: startLine, Column: startColumn},
+ End: Pos{Line: endLine, Column: endColumn},
+ Text: text,
+ }
+}
+
+func (e Edit) toProtocolChangeEvent() protocol.TextDocumentContentChangeEvent {
+ return protocol.TextDocumentContentChangeEvent{
+ Range: &protocol.Range{
+ Start: e.Start.toProtocolPosition(),
+ End: e.End.toProtocolPosition(),
+ },
+ Text: e.Text,
+ }
+}
+
+func fromProtocolTextEdit(textEdit protocol.TextEdit) Edit {
+ return Edit{
+ Start: fromProtocolPosition(textEdit.Range.Start),
+ End: fromProtocolPosition(textEdit.Range.End),
+ Text: textEdit.NewText,
+ }
+}
+
+// inText reports whether p is a valid position in the text buffer.
+func inText(p Pos, content []string) bool {
+ if p.Line < 0 || p.Line >= len(content) {
+ return false
+ }
+ // Note the strict right bound: the column indexes character _separators_,
+ // not characters.
+ if p.Column < 0 || p.Column > len(content[p.Line]) {
+ return false
+ }
+ return true
+}
+
+// editContent implements a simplistic, inefficient algorithm for applying text
+// edits to our buffer representation. It returns an error if the edit is
+// invalid for the current content.
+func editContent(content []string, edit Edit) ([]string, error) {
+ if edit.End.Line < edit.Start.Line || (edit.End.Line == edit.Start.Line && edit.End.Column < edit.Start.Column) {
+ return nil, fmt.Errorf("invalid edit: end %v before start %v", edit.End, edit.Start)
+ }
+ if !inText(edit.Start, content) {
+ return nil, fmt.Errorf("start position %v is out of bounds", edit.Start)
+ }
+ if !inText(edit.End, content) {
+ return nil, fmt.Errorf("end position %v is out of bounds", edit.End)
+ }
+ // Splice the edit text in between the first and last lines of the edit.
+ prefix := string([]rune(content[edit.Start.Line])[:edit.Start.Column])
+ suffix := string([]rune(content[edit.End.Line])[edit.End.Column:])
+ newLines := strings.Split(prefix+edit.Text+suffix, "\n")
+ newContent := append(content[:edit.Start.Line], newLines...)
+ return append(newContent, content[edit.End.Line+1:]...), nil
+}
diff --git a/internal/lsp/fake/edit_test.go b/internal/lsp/fake/edit_test.go
new file mode 100644
index 0000000..12789eb
--- /dev/null
+++ b/internal/lsp/fake/edit_test.go
@@ -0,0 +1,97 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fake
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestApplyEdit(t *testing.T) {
+ tests := []struct {
+ label string
+ content string
+ edit Edit
+ want string
+ wantErr bool
+ }{
+ {
+ label: "empty content",
+ },
+ {
+ label: "empty edit",
+ content: "hello",
+ edit: Edit{},
+ want: "hello",
+ },
+ {
+ label: "unicode edit",
+ content: "hello, 日本語",
+ edit: Edit{
+ Start: Pos{Line: 0, Column: 7},
+ End: Pos{Line: 0, Column: 10},
+ Text: "world",
+ },
+ want: "hello, world",
+ },
+ {
+ label: "range edit",
+ content: "ABC\nDEF\nGHI\nJKL",
+ edit: Edit{
+ Start: Pos{Line: 1, Column: 1},
+ End: Pos{Line: 2, Column: 3},
+ Text: "12\n345",
+ },
+ want: "ABC\nD12\n345\nJKL",
+ },
+ {
+ label: "end before start",
+ content: "ABC\nDEF\nGHI\nJKL",
+ edit: Edit{
+ End: Pos{Line: 1, Column: 1},
+ Start: Pos{Line: 2, Column: 3},
+ Text: "12\n345",
+ },
+ wantErr: true,
+ },
+ {
+ label: "out of bounds line",
+ content: "ABC\nDEF\nGHI\nJKL",
+ edit: Edit{
+ Start: Pos{Line: 1, Column: 1},
+ End: Pos{Line: 4, Column: 3},
+ Text: "12\n345",
+ },
+ wantErr: true,
+ },
+ {
+ label: "out of bounds column",
+ content: "ABC\nDEF\nGHI\nJKL",
+ edit: Edit{
+ Start: Pos{Line: 1, Column: 4},
+ End: Pos{Line: 2, Column: 3},
+ Text: "12\n345",
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, test := range tests {
+ test := test
+ t.Run(test.label, func(t *testing.T) {
+ lines := strings.Split(test.content, "\n")
+ newLines, err := editContent(lines, test.edit)
+ if (err != nil) != test.wantErr {
+ t.Errorf("got err %v, want error: %t", err, test.wantErr)
+ }
+ if err != nil {
+ return
+ }
+ if got := strings.Join(newLines, "\n"); got != test.want {
+ t.Errorf("got %q, want %q", got, test.want)
+ }
+ })
+ }
+}
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
new file mode 100644
index 0000000..5cb0a57
--- /dev/null
+++ b/internal/lsp/fake/editor.go
@@ -0,0 +1,371 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fake
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync"
+
+ "golang.org/x/tools/internal/jsonrpc2"
+ "golang.org/x/tools/internal/lsp/protocol"
+)
+
+// Editor is a fake editor client. It keeps track of client state and can be
+// used for writing LSP tests.
+type Editor struct {
+ // server, client, and workspace are concurrency safe and written only at
+ // construction, so do not require synchronization.
+ server protocol.Server
+ client *Client
+ ws *Workspace
+
+ // Since this editor is intended just for testing, we use very coarse
+ // locking.
+ mu sync.Mutex
+ // Editor state.
+ buffers map[string]buffer
+ lastMessage *protocol.ShowMessageParams
+ logs []*protocol.LogMessageParams
+ diagnostics *protocol.PublishDiagnosticsParams
+ events []interface{}
+ // Capabilities / Options
+ serverCapabilities protocol.ServerCapabilities
+}
+
+type buffer struct {
+ version int
+ path string
+ content []string
+}
+
+func (b buffer) text() string {
+ return strings.Join(b.content, "\n")
+}
+
+// NewConnectedEditor creates a new editor that dispatches the LSP across the
+// provided jsonrpc2 connection.
+//
+// The returned editor is initialized and ready to use.
+func NewConnectedEditor(ctx context.Context, ws *Workspace, conn *jsonrpc2.Conn) (*Editor, error) {
+ e := NewEditor(ws)
+ e.server = protocol.ServerDispatcher(conn)
+ e.client = &Client{Editor: e}
+ conn.AddHandler(protocol.ClientHandler(e.client))
+ if err := e.initialize(ctx); err != nil {
+ return nil, err
+ }
+ e.ws.AddWatcher(e.onFileChanges)
+ return e, nil
+}
+
+// NewEditor Creates a new Editor.
+func NewEditor(ws *Workspace) *Editor {
+ return &Editor{
+ buffers: make(map[string]buffer),
+ ws: ws,
+ }
+}
+
+// Shutdown issues the 'shutdown' LSP notification.
+func (e *Editor) Shutdown(ctx context.Context) error {
+ if e.server != nil {
+ if err := e.server.Shutdown(ctx); err != nil {
+ return fmt.Errorf("Shutdown: %v", err)
+ }
+ }
+ return nil
+}
+
+// Exit issues the 'exit' LSP notification.
+func (e *Editor) Exit(ctx context.Context) error {
+ if e.server != nil {
+ // Not all LSP clients issue the exit RPC, but we do so here to ensure that
+ // we gracefully handle it on multi-session servers.
+ if err := e.server.Exit(ctx); err != nil {
+ return fmt.Errorf("Exit: %v", err)
+ }
+ }
+ return nil
+}
+
+// Client returns the LSP client for this editor.
+func (e *Editor) Client() *Client {
+ return e.client
+}
+
+func (e *Editor) configuration() map[string]interface{} {
+ return map[string]interface{}{
+ "env": map[string]interface{}{
+ "GOPATH": e.ws.GOPATH(),
+ "GO111MODULE": "on",
+ },
+ }
+}
+
+func (e *Editor) initialize(ctx context.Context) error {
+ params := &protocol.ParamInitialize{}
+ params.ClientInfo.Name = "fakeclient"
+ params.ClientInfo.Version = "v1.0.0"
+ params.RootURI = e.ws.RootURI()
+
+ // TODO: set client capabilities.
+ params.Trace = "messages"
+ // TODO: support workspace folders.
+
+ if e.server != nil {
+ resp, err := e.server.Initialize(ctx, params)
+ if err != nil {
+ return fmt.Errorf("initialize: %v", err)
+ }
+ e.mu.Lock()
+ e.serverCapabilities = resp.Capabilities
+ e.mu.Unlock()
+
+ if err := e.server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
+ return fmt.Errorf("initialized: %v", err)
+ }
+ }
+ return nil
+}
+
+func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
+ if e.server == nil {
+ return
+ }
+ var lspevts []protocol.FileEvent
+ for _, evt := range evts {
+ lspevts = append(lspevts, evt.ProtocolEvent)
+ }
+ e.server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
+ Changes: lspevts,
+ })
+}
+
+// OpenFile creates a buffer for the given workspace-relative file.
+func (e *Editor) OpenFile(ctx context.Context, path string) error {
+ content, err := e.ws.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ buf := newBuffer(path, content)
+ e.mu.Lock()
+ e.buffers[path] = buf
+ item := textDocumentItem(e.ws, buf)
+ e.mu.Unlock()
+
+ if e.server != nil {
+ if err := e.server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
+ TextDocument: item,
+ }); err != nil {
+ return fmt.Errorf("DidOpen: %v", err)
+ }
+ }
+ return nil
+}
+
+func newBuffer(path, content string) buffer {
+ return buffer{
+ version: 1,
+ path: path,
+ content: strings.Split(content, "\n"),
+ }
+}
+
+func textDocumentItem(ws *Workspace, buf buffer) protocol.TextDocumentItem {
+ uri := ws.URI(buf.path)
+ languageID := ""
+ if strings.HasSuffix(buf.path, ".go") {
+ // TODO: what about go.mod files? What is their language ID?
+ languageID = "go"
+ }
+ return protocol.TextDocumentItem{
+ URI: uri,
+ LanguageID: languageID,
+ Version: float64(buf.version),
+ Text: buf.text(),
+ }
+}
+
+// CreateBuffer creates a new unsaved buffer corresponding to the workspace
+// path, containing the given textual content.
+func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
+ buf := newBuffer(path, content)
+ e.mu.Lock()
+ e.buffers[path] = buf
+ item := textDocumentItem(e.ws, buf)
+ e.mu.Unlock()
+
+ if e.server != nil {
+ if err := e.server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
+ TextDocument: item,
+ }); err != nil {
+ return fmt.Errorf("DidOpen: %v", err)
+ }
+ }
+ return nil
+}
+
+// CloseBuffer removes the current buffer (regardless of whether it is saved).
+func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
+ e.mu.Lock()
+ _, ok := e.buffers[path]
+ if !ok {
+ e.mu.Unlock()
+ return fmt.Errorf("unknown path %q", path)
+ }
+ delete(e.buffers, path)
+ e.mu.Unlock()
+
+ if e.server != nil {
+ if err := e.server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
+ TextDocument: protocol.TextDocumentIdentifier{
+ URI: e.ws.URI(path),
+ },
+ }); err != nil {
+ return fmt.Errorf("DidClose: %v", err)
+ }
+ }
+ return nil
+}
+
+// WriteBuffer writes the content of the buffer specified by the given path to
+// the filesystem.
+func (e *Editor) WriteBuffer(ctx context.Context, path string) error {
+ e.mu.Lock()
+ buf, ok := e.buffers[path]
+ if !ok {
+ e.mu.Unlock()
+ return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path))
+ }
+ content := buf.text()
+ includeText := false
+ syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions)
+ if ok {
+ includeText = syncOptions.Save.IncludeText
+ }
+ e.mu.Unlock()
+
+ docID := protocol.TextDocumentIdentifier{
+ URI: e.ws.URI(buf.path),
+ }
+ if e.server != nil {
+ if err := e.server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
+ TextDocument: docID,
+ Reason: protocol.Manual,
+ }); err != nil {
+ return fmt.Errorf("WillSave: %v", err)
+ }
+ }
+ if err := e.ws.WriteFile(ctx, path, content); err != nil {
+ return fmt.Errorf("writing %q: %v", path, err)
+ }
+ if e.server != nil {
+ params := &protocol.DidSaveTextDocumentParams{
+ TextDocument: protocol.VersionedTextDocumentIdentifier{
+ Version: float64(buf.version),
+ TextDocumentIdentifier: docID,
+ },
+ }
+ if includeText {
+ params.Text = &content
+ }
+ if err := e.server.DidSave(ctx, params); err != nil {
+ return fmt.Errorf("DidSave: %v", err)
+ }
+ }
+ return nil
+}
+
+// EditBuffer applies the given test edits to the buffer identified by path.
+func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error {
+ params, err := e.doEdits(ctx, path, edits)
+ if err != nil {
+ return err
+ }
+ if e.server != nil {
+ if err := e.server.DidChange(ctx, params); err != nil {
+ return fmt.Errorf("DidChange: %v", err)
+ }
+ }
+ return nil
+}
+
+func (e *Editor) doEdits(ctx context.Context, path string, edits []Edit) (*protocol.DidChangeTextDocumentParams, error) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ buf, ok := e.buffers[path]
+ if !ok {
+ return nil, fmt.Errorf("unknown buffer %q", path)
+ }
+ var (
+ content = make([]string, len(buf.content))
+ err error
+ evts []protocol.TextDocumentContentChangeEvent
+ )
+ copy(content, buf.content)
+ for _, edit := range edits {
+ content, err = editContent(content, edit)
+ if err != nil {
+ return nil, err
+ }
+ evts = append(evts, edit.toProtocolChangeEvent())
+ }
+ buf.content = content
+ buf.version++
+ e.buffers[path] = buf
+ params := &protocol.DidChangeTextDocumentParams{
+ TextDocument: protocol.VersionedTextDocumentIdentifier{
+ Version: float64(buf.version),
+ TextDocumentIdentifier: protocol.TextDocumentIdentifier{
+ URI: e.ws.URI(buf.path),
+ },
+ },
+ ContentChanges: evts,
+ }
+ return params, nil
+}
+
+// GoToDefinition jumps to the definition of the symbol at the given position
+// in an open buffer.
+func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) {
+ if err := e.checkBufferPosition(path, pos); err != nil {
+ return "", Pos{}, err
+ }
+ params := &protocol.DefinitionParams{}
+ params.TextDocument.URI = e.ws.URI(path)
+ params.Position = pos.toProtocolPosition()
+
+ resp, err := e.server.Definition(ctx, params)
+ if err != nil {
+ return "", Pos{}, fmt.Errorf("definition: %v", err)
+ }
+ if len(resp) == 0 {
+ return "", Pos{}, nil
+ }
+ newPath := e.ws.URIToPath(resp[0].URI)
+ newPos := fromProtocolPosition(resp[0].Range.Start)
+ if err := e.OpenFile(ctx, newPath); err != nil {
+ return "", Pos{}, fmt.Errorf("OpenFile: %v", err)
+ }
+ return newPath, newPos, nil
+}
+
+func (e *Editor) checkBufferPosition(path string, pos Pos) error {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ buf, ok := e.buffers[path]
+ if !ok {
+ return fmt.Errorf("buffer %q is not open", path)
+ }
+ if !inText(pos, buf.content) {
+ return fmt.Errorf("position %v is invalid in buffer %q", pos, path)
+ }
+ return nil
+}
+
+// TODO: expose more client functionality, for example Hover, CodeAction,
+// Rename, Completion, etc. setting the content of an entire buffer, etc.
diff --git a/internal/lsp/fake/editor_test.go b/internal/lsp/fake/editor_test.go
new file mode 100644
index 0000000..544f809
--- /dev/null
+++ b/internal/lsp/fake/editor_test.go
@@ -0,0 +1,60 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fake
+
+import (
+ "context"
+ "testing"
+)
+
+const exampleProgram = `
+-- go.mod --
+go 1.12
+-- main.go --
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello World.")
+}
+`
+
+func TestClientEditing(t *testing.T) {
+ ws, err := NewWorkspace("test", []byte(exampleProgram))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ws.Close()
+ ctx := context.Background()
+ client := NewEditor(ws)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := client.OpenFile(ctx, "main.go"); err != nil {
+ t.Fatal(err)
+ }
+ if err := client.EditBuffer(ctx, "main.go", []Edit{
+ {
+ Start: Pos{5, 14},
+ End: Pos{5, 26},
+ Text: "Hola, mundo.",
+ },
+ }); err != nil {
+ t.Fatal(err)
+ }
+ got := client.buffers["main.go"].text()
+ want := `package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hola, mundo.")
+}
+`
+ if got != want {
+ t.Errorf("got text %q, want %q", got, want)
+ }
+}
diff --git a/internal/lsp/fake/workspace.go b/internal/lsp/fake/workspace.go
new file mode 100644
index 0000000..b540163
--- /dev/null
+++ b/internal/lsp/fake/workspace.go
@@ -0,0 +1,207 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fake
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/span"
+ "golang.org/x/tools/txtar"
+)
+
+// FileEvent wraps the protocol.FileEvent so that it can be associated with a
+// workspace-relative path.
+type FileEvent struct {
+ Path string
+ ProtocolEvent protocol.FileEvent
+}
+
+// The Workspace type represents a temporary workspace to use for editing Go
+// files in tests.
+type Workspace struct {
+ name string
+ gopath string
+ workdir string
+
+ watcherMu sync.Mutex
+ watchers []func(context.Context, []FileEvent)
+}
+
+// NewWorkspace creates a named workspace populated by the txtar-encoded
+// content given by txt. It creates temporary directories for the workspace
+// content and for GOPATH.
+func NewWorkspace(name string, txt []byte) (_ *Workspace, err error) {
+ w := &Workspace{name: name}
+ defer func() {
+ // Clean up if we fail at any point in this constructor.
+ if err != nil {
+ w.removeAll()
+ }
+ }()
+ dir, err := ioutil.TempDir("", fmt.Sprintf("goplstest-ws-%s-", name))
+ if err != nil {
+ return nil, fmt.Errorf("creating temporary workdir: %v", err)
+ }
+ w.workdir = dir
+ gopath, err := ioutil.TempDir("", fmt.Sprintf("goplstest-gopath-%s-", name))
+ if err != nil {
+ return nil, fmt.Errorf("creating temporary gopath: %v", err)
+ }
+ w.gopath = gopath
+ archive := txtar.Parse(txt)
+ for _, f := range archive.Files {
+ if err := w.writeFileData(f.Name, f.Data); err != nil {
+ return nil, err
+ }
+ }
+ return w, nil
+}
+
+// RootURI returns the root URI for this workspace.
+func (w *Workspace) RootURI() protocol.DocumentURI {
+ return toURI(w.workdir)
+}
+
+// GOPATH returns the value that GOPATH should be set to for this workspace.
+func (w *Workspace) GOPATH() string {
+ return w.gopath
+}
+
+// AddWatcher registers the given func to be called on any file change.
+func (w *Workspace) AddWatcher(watcher func(context.Context, []FileEvent)) {
+ w.watcherMu.Lock()
+ w.watchers = append(w.watchers, watcher)
+ w.watcherMu.Unlock()
+}
+
+// filePath returns the absolute filesystem path to a the workspace-relative
+// path.
+func (w *Workspace) filePath(path string) string {
+ fp := filepath.FromSlash(path)
+ if filepath.IsAbs(fp) {
+ return fp
+ }
+ return filepath.Join(w.workdir, filepath.FromSlash(path))
+}
+
+// URI returns the URI to a the workspace-relative path.
+func (w *Workspace) URI(path string) protocol.DocumentURI {
+ return toURI(w.filePath(path))
+}
+
+// URIToPath converts a uri to a workspace-relative path (or an absolute path,
+// if the uri is outside of the workspace).
+func (w *Workspace) URIToPath(uri protocol.DocumentURI) string {
+ root := w.RootURI().SpanURI().Filename()
+ path := uri.SpanURI().Filename()
+ if rel, err := filepath.Rel(root, path); err == nil && !strings.HasPrefix(rel, "..") {
+ return filepath.ToSlash(rel)
+ }
+ return filepath.ToSlash(path)
+}
+
+func toURI(fp string) protocol.DocumentURI {
+ return protocol.DocumentURI(span.URIFromPath(fp))
+}
+
+// ReadFile reads a text file specified by a workspace-relative path.
+func (w *Workspace) ReadFile(path string) (string, error) {
+ b, err := ioutil.ReadFile(w.filePath(path))
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+}
+
+// RemoveFile removes a workspace-relative file path.
+func (w *Workspace) RemoveFile(ctx context.Context, path string) error {
+ fp := w.filePath(path)
+ if err := os.Remove(fp); err != nil {
+ return fmt.Errorf("removing %q: %v", path, err)
+ }
+ evts := []FileEvent{{
+ Path: path,
+ ProtocolEvent: protocol.FileEvent{
+ URI: w.URI(path),
+ Type: protocol.Deleted,
+ },
+ }}
+ w.sendEvents(ctx, evts)
+ return nil
+}
+
+func (w *Workspace) sendEvents(ctx context.Context, evts []FileEvent) {
+ w.watcherMu.Lock()
+ watchers := make([]func(context.Context, []FileEvent), len(w.watchers))
+ copy(watchers, w.watchers)
+ w.watcherMu.Unlock()
+ for _, w := range watchers {
+ go w(ctx, evts)
+ }
+}
+
+// WriteFile writes text file content to a workspace-relative path.
+func (w *Workspace) WriteFile(ctx context.Context, path, content string) error {
+ fp := w.filePath(path)
+ _, err := os.Stat(fp)
+ if err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("checking if %q exists: %v", path, err)
+ }
+ var changeType protocol.FileChangeType
+ if os.IsNotExist(err) {
+ changeType = protocol.Created
+ } else {
+ changeType = protocol.Changed
+ }
+ if err := w.writeFileData(path, []byte(content)); err != nil {
+ return err
+ }
+ evts := []FileEvent{{
+ Path: path,
+ ProtocolEvent: protocol.FileEvent{
+ URI: w.URI(path),
+ Type: changeType,
+ },
+ }}
+ w.sendEvents(ctx, evts)
+ return nil
+}
+
+func (w *Workspace) writeFileData(path string, data []byte) error {
+ fp := w.filePath(path)
+ if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil {
+ return fmt.Errorf("creating nested directory: %v", err)
+ }
+ if err := ioutil.WriteFile(fp, data, 0644); err != nil {
+ return fmt.Errorf("writing %q: %v", path, err)
+ }
+ return nil
+}
+
+func (w *Workspace) removeAll() error {
+ var werr, perr error
+ if w.workdir != "" {
+ werr = os.RemoveAll(w.workdir)
+ }
+ if w.gopath != "" {
+ perr = os.RemoveAll(w.gopath)
+ }
+ if werr != nil || perr != nil {
+ return fmt.Errorf("error(s) cleaning workspace: removing workdir: %v; removing gopath: %v", werr, perr)
+ }
+ return nil
+}
+
+// Close removes all state associated with the workspace.
+func (w *Workspace) Close() error {
+ return w.removeAll()
+}
diff --git a/internal/lsp/fake/workspace_test.go b/internal/lsp/fake/workspace_test.go
new file mode 100644
index 0000000..31b46d2
--- /dev/null
+++ b/internal/lsp/fake/workspace_test.go
@@ -0,0 +1,92 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fake
+
+import (
+ "context"
+ "testing"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+)
+
+const data = `
+-- go.mod --
+go 1.12
+-- nested/README.md --
+Hello World!
+`
+
+func newWorkspace(t *testing.T) (*Workspace, <-chan []FileEvent, func()) {
+ t.Helper()
+
+ ws, err := NewWorkspace("default", []byte(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+ cleanup := func() {
+ if err := ws.Close(); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ fileEvents := make(chan []FileEvent)
+ watch := func(_ context.Context, events []FileEvent) {
+ fileEvents <- events
+ }
+ ws.AddWatcher(watch)
+ return ws, fileEvents, cleanup
+}
+
+func TestWorkspace_ReadFile(t *testing.T) {
+ ws, _, cleanup := newWorkspace(t)
+ defer cleanup()
+
+ got, err := ws.ReadFile("nested/README.md")
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := "Hello World!\n"
+ if got != want {
+ t.Errorf("reading workspace file, got %q, want %q", got, want)
+ }
+}
+
+func TestWorkspace_WriteFile(t *testing.T) {
+ ws, events, cleanup := newWorkspace(t)
+ defer cleanup()
+ ctx := context.Background()
+
+ tests := []struct {
+ path string
+ wantType protocol.FileChangeType
+ }{
+ {"data.txt", protocol.Created},
+ {"nested/README.md", protocol.Changed},
+ }
+
+ for _, test := range tests {
+ if err := ws.WriteFile(ctx, test.path, "42"); err != nil {
+ t.Fatal(err)
+ }
+ es := <-events
+ if got := len(es); got != 1 {
+ t.Fatalf("len(events) = %d, want 1", got)
+ }
+ if es[0].Path != test.path {
+ t.Errorf("event.Path = %q, want %q", es[0].Path, test.path)
+ }
+ if es[0].ProtocolEvent.Type != test.wantType {
+ t.Errorf("event type = %v, want %v", es[0].ProtocolEvent.Type, test.wantType)
+ }
+ got, err := ws.ReadFile(test.path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := "42"
+ if got != want {
+ t.Errorf("ws.ReadFile(%q) = %q, want %q", test.path, got, want)
+ }
+ }
+}
diff --git a/internal/lsp/folding_range.go b/internal/lsp/folding_range.go
index 19b28eb..5bae8f5 100644
--- a/internal/lsp/folding_range.go
+++ b/internal/lsp/folding_range.go
@@ -5,28 +5,15 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
)
func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
- var ranges []*source.FoldingRangeInfo
- switch fh.Identity().Kind {
- case source.Go:
- ranges, err = source.FoldingRange(ctx, snapshot, fh, view.Options().LineFoldingOnly)
- case source.Mod:
- ranges = nil
- }
+ ranges, err := source.FoldingRange(ctx, snapshot, fh, snapshot.View().Options().LineFoldingOnly)
if err != nil {
return nil, err
}
diff --git a/internal/lsp/format.go b/internal/lsp/format.go
index f3abeb1..33dd407 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -9,28 +9,14 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
)
func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
- var edits []protocol.TextEdit
- switch fh.Identity().Kind {
- case source.Go:
- edits, err = source.Format(ctx, snapshot, fh)
- case source.Mod:
- return nil, nil
- }
-
+ edits, err := source.Format(ctx, snapshot, fh)
if err != nil {
return nil, err
}
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 065819d..9ea2f01 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -42,8 +42,8 @@
if len(s.pendingFolders) == 0 {
if params.RootURI != "" {
s.pendingFolders = []protocol.WorkspaceFolder{{
- URI: params.RootURI,
- Name: path.Base(params.RootURI),
+ URI: string(params.RootURI),
+ Name: path.Base(params.RootURI.SpanURI().Filename()),
}}
} else {
// No folders and no root--we are in single file mode.
@@ -79,6 +79,7 @@
ImplementationProvider: true,
DocumentFormattingProvider: true,
DocumentSymbolProvider: true,
+ WorkspaceSymbolProvider: true,
ExecuteCommandProvider: protocol.ExecuteCommandOptions{
Commands: options.SupportedCommands,
},
@@ -164,8 +165,8 @@
viewErrors := make(map[span.URI]error)
for _, folder := range folders {
- uri := span.NewURI(folder.URI)
- _, snapshot, err := s.addView(ctx, folder.Name, span.NewURI(folder.URI))
+ uri := span.URIFromURI(folder.URI)
+ _, snapshot, err := s.addView(ctx, folder.Name, uri)
if err != nil {
viewErrors[uri] = err
continue
@@ -191,10 +192,10 @@
v := protocol.ParamConfiguration{
ConfigurationParams: protocol.ConfigurationParams{
Items: []protocol.ConfigurationItem{{
- ScopeURI: protocol.NewURI(folder),
+ ScopeURI: string(folder),
Section: "gopls",
}, {
- ScopeURI: protocol.NewURI(folder),
+ ScopeURI: string(folder),
Section: fmt.Sprintf("gopls-%s", name),
}},
},
@@ -233,25 +234,57 @@
return nil
}
+// beginFileRequest checks preconditions for a file-oriented request and routes
+// it to a snapshot.
+// We don't want to return errors for benign conditions like wrong file type,
+// so callers should do if !ok { return err } rather than if err != nil.
+func (s *Server) beginFileRequest(pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.FileHandle, bool, error) {
+ uri := pURI.SpanURI()
+ if !uri.IsFile() {
+ // Not a file URI. Stop processing the request, but don't return an error.
+ return nil, nil, false, nil
+ }
+ view, err := s.session.ViewOf(uri)
+ if err != nil {
+ return nil, nil, false, err
+ }
+ snapshot := view.Snapshot()
+ fh, err := snapshot.GetFile(uri)
+ if err != nil {
+ return nil, nil, false, err
+ }
+ if expectKind != source.UnknownKind && fh.Identity().Kind != expectKind {
+ // Wrong kind of file. Nothing to do.
+ return nil, nil, false, nil
+ }
+ return snapshot, fh, true, nil
+}
+
func (s *Server) shutdown(ctx context.Context) error {
s.stateMu.Lock()
defer s.stateMu.Unlock()
if s.state < serverInitialized {
return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server not initialized")
}
- // drop all the active views
- s.session.Shutdown(ctx)
- s.state = serverShutDown
+ if s.state != serverShutDown {
+ // drop all the active views
+ s.session.Shutdown(ctx)
+ s.state = serverShutDown
+ }
return nil
}
+// ServerExitFunc is used to exit when requested by the client. It is mutable
+// for testing purposes.
+var ServerExitFunc = os.Exit
+
func (s *Server) exit(ctx context.Context) error {
s.stateMu.Lock()
defer s.stateMu.Unlock()
if s.state != serverShutDown {
- os.Exit(1)
+ ServerExitFunc(1)
}
- os.Exit(0)
+ ServerExitFunc(0)
return nil
}
diff --git a/internal/lsp/helper/README.md b/internal/lsp/helper/README.md
index e27e769..3c51efe 100644
--- a/internal/lsp/helper/README.md
+++ b/internal/lsp/helper/README.md
@@ -3,7 +3,7 @@
`helper` generates boilerplate code for server.go by processing the
generated code in `protocol/tsserver.go`.
-First, build `helper` in this directore (`go build .`).
+First, build `helper` in this directory (`go build .`).
In directory `lsp`, executing `go generate server.go` generates the stylized file
`server_gen.go` that contains stubs for type `Server`.
diff --git a/internal/lsp/helper/helper.go b/internal/lsp/helper/helper.go
index e90f2f4..06b2457 100644
--- a/internal/lsp/helper/helper.go
+++ b/internal/lsp/helper/helper.go
@@ -7,6 +7,7 @@
"flag"
"fmt"
"go/ast"
+ "go/format"
"go/parser"
"go/token"
"log"
@@ -109,7 +110,10 @@
if err != nil {
log.Fatal(err)
}
- ans := bytes.Replace(buf.Bytes(), []byte("\\\n"), []byte{}, -1)
+ ans, err := format.Source(bytes.Replace(buf.Bytes(), []byte("\\\n"), []byte{}, -1))
+ if err != nil {
+ log.Fatal(err)
+ }
fd.Write(ans)
}
diff --git a/internal/lsp/highlight.go b/internal/lsp/highlight.go
index 45e374b..7386ddc 100644
--- a/internal/lsp/highlight.go
+++ b/internal/lsp/highlight.go
@@ -10,31 +10,17 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
- "golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
)
func (s *Server) documentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
+ rngs, err := source.Highlight(ctx, snapshot, fh, params.Position)
if err != nil {
- return nil, err
- }
- var rngs []protocol.Range
- switch fh.Identity().Kind {
- case source.Go:
- rngs, err = source.Highlight(ctx, snapshot, fh, params.Position)
- case source.Mod:
- return nil, nil
- }
-
- if err != nil {
- log.Error(ctx, "no highlight", err, telemetry.URI.Of(uri))
+ log.Error(ctx, "no highlight", err, telemetry.URI.Of(params.TextDocument.URI))
}
return toProtocolHighlight(rngs), nil
}
diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go
index 967907a..32af7e2 100644
--- a/internal/lsp/hover.go
+++ b/internal/lsp/hover.go
@@ -7,46 +7,21 @@
import (
"context"
+ "golang.org/x/tools/internal/lsp/mod"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
)
func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
+ switch fh.Identity().Kind {
+ case source.Mod:
+ return mod.Hover(ctx, snapshot, fh, params.Position)
+ case source.Go:
+ return source.Hover(ctx, snapshot, fh, params.Position)
}
- if fh.Identity().Kind != source.Go {
- return nil, nil
- }
- ident, err := source.Identifier(ctx, snapshot, fh, params.Position, source.WidestPackageHandle)
- if err != nil {
- return nil, nil
- }
- h, err := ident.Hover(ctx)
- if err != nil {
- return nil, err
- }
- rng, err := ident.Range()
- if err != nil {
- return nil, err
- }
- hover, err := source.FormatHover(h, view.Options())
- if err != nil {
- return nil, err
- }
- return &protocol.Hover{
- Contents: protocol.MarkupContent{
- Kind: view.Options().PreferredContentFormat,
- Value: hover,
- },
- Range: rng,
- }, nil
+ return nil, nil
}
diff --git a/internal/lsp/implementation.go b/internal/lsp/implementation.go
index bae2832..e4b3650 100644
--- a/internal/lsp/implementation.go
+++ b/internal/lsp/implementation.go
@@ -9,22 +9,12 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
)
func (s *Server) implementation(ctx context.Context, params *protocol.ImplementationParams) ([]protocol.Location, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
- if fh.Identity().Kind != source.Go {
- return nil, nil
- }
return source.Implementation(ctx, snapshot, fh, params.Position)
}
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index ddcb334..ae7a0a5 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -5,6 +5,7 @@
package lsp
import (
+ "bytes"
"context"
"fmt"
"go/ast"
@@ -12,6 +13,7 @@
"net/url"
"regexp"
"strconv"
+ "strings"
"sync"
"golang.org/x/tools/internal/lsp/protocol"
@@ -21,20 +23,78 @@
)
func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
- return nil, err
- }
- fh, err := view.Snapshot().GetFile(uri)
- if err != nil {
- return nil, err
- }
// TODO(golang/go#36501): Support document links for go.mod files.
- if fh.Identity().Kind == source.Mod {
- return nil, nil
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
+ if !ok {
+ return nil, err
}
- file, m, _, err := view.Session().Cache().ParseGoHandle(fh, source.ParseFull).Parse(ctx)
+ switch fh.Identity().Kind {
+ case source.Mod:
+ return modLinks(ctx, snapshot, fh)
+ case source.Go:
+ return goLinks(ctx, snapshot.View(), fh)
+ }
+ return nil, nil
+}
+
+func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
+ view := snapshot.View()
+
+ file, m, err := snapshot.ModHandle(ctx, fh).Parse(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var links []protocol.DocumentLink
+ for _, req := range file.Require {
+ dep := []byte(req.Mod.Path)
+ s, e := req.Syntax.Start.Byte, req.Syntax.End.Byte
+ i := bytes.Index(m.Content[s:e], dep)
+ if i == -1 {
+ continue
+ }
+ // Shift the start position to the location of the
+ // dependency within the require statement.
+ start, end := token.Pos(s+i), token.Pos(s+i+len(dep))
+ target := fmt.Sprintf("https://%s/mod/%s", view.Options().LinkTarget, req.Mod.String())
+ if l, err := toProtocolLink(view, m, target, start, end, source.Mod); err == nil {
+ links = append(links, l)
+ } else {
+ log.Error(ctx, "failed to create protocol link", err)
+ }
+ }
+ // TODO(ridersofrohan): handle links for replace and exclude directives
+ if syntax := file.Syntax; syntax == nil {
+ return links, nil
+ }
+ // Get all the links that are contained in the comments of the file.
+ for _, expr := range file.Syntax.Stmt {
+ comments := expr.Comment()
+ if comments == nil {
+ continue
+ }
+ for _, cmt := range comments.Before {
+ links = append(links, findLinksInString(ctx, view, cmt.Token, token.Pos(cmt.Start.Byte), m, source.Mod)...)
+ }
+ for _, cmt := range comments.Suffix {
+ links = append(links, findLinksInString(ctx, view, cmt.Token, token.Pos(cmt.Start.Byte), m, source.Mod)...)
+ }
+ for _, cmt := range comments.After {
+ links = append(links, findLinksInString(ctx, view, cmt.Token, token.Pos(cmt.Start.Byte), m, source.Mod)...)
+ }
+ }
+ return links, nil
+}
+
+func goLinks(ctx context.Context, view source.View, fh source.FileHandle) ([]protocol.DocumentLink, error) {
+ phs, err := view.Snapshot().PackageHandles(ctx, fh)
+ if err != nil {
+ return nil, err
+ }
+ ph, err := source.WidestPackageHandle(phs)
+ if err != nil {
+ return nil, err
+ }
+ file, _, m, _, err := view.Session().Cache().ParseGoHandle(fh, source.ParseFull).Parse(ctx)
if err != nil {
return nil, err
}
@@ -44,11 +104,13 @@
case *ast.ImportSpec:
// For import specs, provide a link to a documentation website, like https://pkg.go.dev.
if target, err := strconv.Unquote(n.Path.Value); err == nil {
+ if mod, version, ok := moduleAtVersion(ctx, target, ph); ok && strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" {
+ target = strings.Replace(target, mod, mod+"@"+version, 1)
+ }
target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target)
-
// Account for the quotation marks in the positions.
start, end := n.Path.Pos()+1, n.Path.End()-1
- if l, err := toProtocolLink(view, m, target, start, end); err == nil {
+ if l, err := toProtocolLink(view, m, target, start, end, source.Go); err == nil {
links = append(links, l)
} else {
log.Error(ctx, "failed to create protocol link", err)
@@ -58,7 +120,7 @@
case *ast.BasicLit:
// Look for links in string literals.
if n.Kind == token.STRING {
- links = append(links, findLinksInString(ctx, view, n.Value, n.Pos(), m)...)
+ links = append(links, findLinksInString(ctx, view, n.Value, n.Pos(), m, source.Go)...)
}
return false
}
@@ -67,13 +129,32 @@
// Look for links in comments.
for _, commentGroup := range file.Comments {
for _, comment := range commentGroup.List {
- links = append(links, findLinksInString(ctx, view, comment.Text, comment.Pos(), m)...)
+ links = append(links, findLinksInString(ctx, view, comment.Text, comment.Pos(), m, source.Go)...)
}
}
return links, nil
}
-func findLinksInString(ctx context.Context, view source.View, src string, pos token.Pos, m *protocol.ColumnMapper) []protocol.DocumentLink {
+func moduleAtVersion(ctx context.Context, target string, ph source.PackageHandle) (string, string, bool) {
+ pkg, err := ph.Check(ctx)
+ if err != nil {
+ return "", "", false
+ }
+ impPkg, err := pkg.GetImport(target)
+ if err != nil {
+ return "", "", false
+ }
+ if impPkg.Module() == nil {
+ return "", "", false
+ }
+ version, modpath := impPkg.Module().Version, impPkg.Module().Path
+ if modpath == "" || version == "" {
+ return "", "", false
+ }
+ return modpath, version, true
+}
+
+func findLinksInString(ctx context.Context, view source.View, src string, pos token.Pos, m *protocol.ColumnMapper, fileKind source.FileKind) []protocol.DocumentLink {
var links []protocol.DocumentLink
for _, index := range view.Options().URLRegexp.FindAllIndex([]byte(src), -1) {
start, end := index[0], index[1]
@@ -88,7 +169,7 @@
if url.Scheme == "" {
url.Scheme = "https"
}
- l, err := toProtocolLink(view, m, url.String(), startPos, endPos)
+ l, err := toProtocolLink(view, m, url.String(), startPos, endPos, fileKind)
if err != nil {
log.Error(ctx, "failed to create protocol link", err)
continue
@@ -107,7 +188,7 @@
}
org, repo, number := matches[1], matches[2], matches[3]
target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number)
- l, err := toProtocolLink(view, m, target, startPos, endPos)
+ l, err := toProtocolLink(view, m, target, startPos, endPos, fileKind)
if err != nil {
log.Error(ctx, "failed to create protocol link", err)
continue
@@ -129,14 +210,34 @@
issueRegexp *regexp.Regexp
)
-func toProtocolLink(view source.View, m *protocol.ColumnMapper, target string, start, end token.Pos) (protocol.DocumentLink, error) {
- spn, err := span.NewRange(view.Session().Cache().FileSet(), start, end).Span()
- if err != nil {
- return protocol.DocumentLink{}, err
- }
- rng, err := m.Range(spn)
- if err != nil {
- return protocol.DocumentLink{}, err
+func toProtocolLink(view source.View, m *protocol.ColumnMapper, target string, start, end token.Pos, fileKind source.FileKind) (protocol.DocumentLink, error) {
+ var rng protocol.Range
+ switch fileKind {
+ case source.Go:
+ spn, err := span.NewRange(view.Session().Cache().FileSet(), start, end).Span()
+ if err != nil {
+ return protocol.DocumentLink{}, err
+ }
+ rng, err = m.Range(spn)
+ if err != nil {
+ return protocol.DocumentLink{}, err
+ }
+ case source.Mod:
+ s, e := int(start), int(end)
+ line, col, err := m.Converter.ToPosition(s)
+ if err != nil {
+ return protocol.DocumentLink{}, err
+ }
+ start := span.NewPoint(line, col, s)
+ line, col, err = m.Converter.ToPosition(e)
+ if err != nil {
+ return protocol.DocumentLink{}, err
+ }
+ end := span.NewPoint(line, col, e)
+ rng, err = m.Range(span.New(m.URI, start, end))
+ if err != nil {
+ return protocol.DocumentLink{}, err
+ }
}
return protocol.DocumentLink{
Range: rng,
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 7617126..b10b073 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -5,15 +5,12 @@
package lsp
import (
- "bytes"
"context"
"fmt"
"go/token"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
- "runtime"
"sort"
"strings"
"testing"
@@ -39,61 +36,95 @@
}
type runner struct {
- server *Server
- data *tests.Data
- ctx context.Context
+ server *Server
+ data *tests.Data
+ diagnostics map[span.URI][]source.Diagnostic
+ ctx context.Context
}
-const viewName = "lsp_test"
-
func testLSP(t *testing.T, exporter packagestest.Exporter) {
ctx := tests.Context(t)
data := tests.Load(t, exporter, "testdata")
- defer data.Exported.Cleanup()
- cache := cache.New(nil)
- session := cache.NewSession()
- options := tests.DefaultOptions()
- session.SetOptions(options)
- options.Env = data.Config.Env
- if _, _, err := session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), options); err != nil {
- t.Fatal(err)
- }
- var modifications []source.FileModification
- for filename, content := range data.Config.Overlay {
- kind := source.DetectLanguage("", filename)
- if kind != source.Go {
- continue
+ for _, datum := range data {
+ defer datum.Exported.Cleanup()
+
+ cache := cache.New(nil, nil)
+ session := cache.NewSession()
+ options := tests.DefaultOptions()
+ session.SetOptions(options)
+ options.Env = datum.Config.Env
+ v, _, err := session.NewView(ctx, datum.Config.Dir, span.URIFromPath(datum.Config.Dir), options)
+ if err != nil {
+ t.Fatal(err)
}
- modifications = append(modifications, source.FileModification{
- URI: span.FileURI(filename),
- Action: source.Open,
- Version: -1,
- Text: content,
- LanguageID: "go",
+ // Check to see if the -modfile flag is available, this is basically a check
+ // to see if the go version >= 1.14. Otherwise, the modfile specific tests
+ // will always fail if this flag is not available.
+ for _, flag := range v.Snapshot().Config(ctx).BuildFlags {
+ if strings.Contains(flag, "-modfile=") {
+ datum.ModfileFlagAvailable = true
+ break
+ }
+ }
+ var modifications []source.FileModification
+ for filename, content := range datum.Config.Overlay {
+ kind := source.DetectLanguage("", filename)
+ if kind != source.Go {
+ continue
+ }
+ modifications = append(modifications, source.FileModification{
+ URI: span.URIFromPath(filename),
+ Action: source.Open,
+ Version: -1,
+ Text: content,
+ LanguageID: "go",
+ })
+ }
+ if _, err := session.DidModifyFiles(ctx, modifications); err != nil {
+ t.Fatal(err)
+ }
+ r := &runner{
+ server: NewServer(session, nil),
+ data: datum,
+ ctx: ctx,
+ }
+ t.Run(datum.Folder, func(t *testing.T) {
+ t.Helper()
+ tests.Run(t, r, datum)
})
}
- if _, err := session.DidModifyFiles(ctx, modifications); err != nil {
- t.Fatal(err)
- }
- r := &runner{
- server: &Server{
- session: session,
- delivered: map[span.URI]sentDiagnostics{},
- },
- data: data,
- ctx: ctx,
- }
- tests.Run(t, r, data)
}
-// TODO: Actually test the LSP diagnostics function in this test.
-func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []source.Diagnostic) {
- v := r.server.session.View(viewName)
- _, got, err := source.FileDiagnostics(r.ctx, v.Snapshot(), uri)
+func (r *runner) CodeLens(t *testing.T, spn span.Span, want []protocol.CodeLens) {
+ if source.DetectLanguage("", spn.URI().Filename()) != source.Mod {
+ return
+ }
+ v, err := r.server.session.ViewOf(spn.URI())
if err != nil {
t.Fatal(err)
}
+ got, err := mod.CodeLens(r.ctx, v.Snapshot(), spn.URI())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff := tests.DiffCodeLens(spn.URI(), want, got); diff != "" {
+ t.Error(diff)
+ }
+}
+
+func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []source.Diagnostic) {
+ // Get the diagnostics for this view if we have not done it before.
+ if r.diagnostics == nil {
+ r.diagnostics = make(map[span.URI][]source.Diagnostic)
+ v := r.server.session.View(r.data.Config.Dir)
+ // Always run diagnostics with analysis.
+ reports := r.server.diagnose(r.ctx, v.Snapshot(), true)
+ for key, diags := range reports {
+ r.diagnostics[key.id.URI] = diags
+ }
+ }
+ got := r.diagnostics[uri]
// A special case to test that there are no diagnostics for a file.
if len(want) == 1 && want[0].Source == "no_diagnostics" {
if len(got) != 0 {
@@ -124,7 +155,7 @@
}
ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
})
if err != nil {
@@ -142,7 +173,7 @@
}
ranges, err = r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
})
if err != nil {
@@ -276,7 +307,7 @@
edits, err := r.server.Formatting(r.ctx, &protocol.DocumentFormattingParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
})
if err != nil {
@@ -304,7 +335,7 @@
filename := uri.Filename()
actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
})
if err != nil {
@@ -332,23 +363,47 @@
func (r *runner) SuggestedFix(t *testing.T, spn span.Span) {
uri := spn.URI()
- filename := uri.Filename()
view, err := r.server.session.ViewOf(uri)
if err != nil {
t.Fatal(err)
}
- snapshot := view.Snapshot()
- _, diagnostics, err := source.FileDiagnostics(r.ctx, snapshot, uri)
+ m, err := r.data.Mapper(uri)
if err != nil {
t.Fatal(err)
}
+ rng, err := m.Range(spn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Get the diagnostics for this view if we have not done it before.
+ if r.diagnostics == nil {
+ r.diagnostics = make(map[span.URI][]source.Diagnostic)
+ // Always run diagnostics with analysis.
+ reports := r.server.diagnose(r.ctx, view.Snapshot(), true)
+ for key, diags := range reports {
+ r.diagnostics[key.id.URI] = diags
+ }
+ }
+ var diag *source.Diagnostic
+ for _, d := range r.diagnostics[uri] {
+ // Compare the start positions rather than the entire range because
+ // some diagnostics have a range with the same start and end position (8:1-8:1).
+ // The current marker functionality prevents us from having a range of 0 length.
+ if protocol.ComparePosition(d.Range.Start, rng.Start) == 0 {
+ diag = &d
+ break
+ }
+ }
+ if diag == nil {
+ t.Fatalf("could not get any suggested fixes for %v", spn)
+ }
actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
Context: protocol.CodeActionContext{
Only: []protocol.CodeActionKind{protocol.QuickFix},
- Diagnostics: toProtocolDiagnostics(diagnostics),
+ Diagnostics: toProtocolDiagnostics([]source.Diagnostic{*diag}),
},
})
if err != nil {
@@ -358,19 +413,17 @@
if len(actions) == 0 {
t.Fatal("no code actions returned")
}
- if len(actions) > 1 {
- t.Fatal("expected only 1 code action")
- }
res, err := applyWorkspaceEdits(r, actions[0].Edit)
if err != nil {
t.Fatal(err)
}
- got := res[uri]
- fixed := string(r.data.Golden("suggestedfix", filename, func() ([]byte, error) {
- return []byte(got), nil
- }))
- if fixed != got {
- t.Errorf("suggested fixes failed for %s, expected:\n%v\ngot:\n%v", filename, fixed, got)
+ for u, got := range res {
+ fixed := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) {
+ return []byte(got), nil
+ }))
+ if fixed != got {
+ t.Errorf("suggested fixes failed for %s, expected:\n%#v\ngot:\n%#v", u.Filename(), fixed, got)
+ }
}
}
@@ -426,7 +479,7 @@
}
if !d.OnlyHover {
didSomething = true
- locURI := span.NewURI(locs[0].URI)
+ locURI := locs[0].URI.SpanURI()
lm, err := r.data.Mapper(locURI)
if err != nil {
t.Fatal(err)
@@ -469,7 +522,7 @@
var results []span.Span
for i := range locs {
- locURI := span.NewURI(locs[i].URI)
+ locURI := locs[i].URI.SpanURI()
lm, err := r.data.Mapper(locURI)
if err != nil {
t.Fatal(err)
@@ -607,7 +660,7 @@
wedit, err := r.server.Rename(r.ctx, &protocol.RenameParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
Position: loc.Range.Start,
NewName: newText,
@@ -636,7 +689,7 @@
if i != 0 {
got += "\n"
}
- uri := span.URI(orderedURIs[i])
+ uri := span.URIFromURI(orderedURIs[i])
if len(res) > 1 {
got += filepath.Base(uri.Filename()) + ":\n"
}
@@ -675,25 +728,21 @@
return
}
// we all love typed nils
- if got == nil || got.(*protocol.Range) == nil {
+ if got == nil {
if want.Text != "" { // expected an ident.
t.Errorf("prepare rename failed for %v: got nil", src)
}
return
}
- xx, ok := got.(*protocol.Range)
- if !ok {
- t.Fatalf("got %T, wanted Range", got)
- }
- if xx.Start == xx.End {
+ if got.Start == got.End {
// Special case for 0-length ranges. Marks can't specify a 0-length range,
// so just compare the start.
- if xx.Start != want.Range.Start {
- t.Errorf("prepare rename failed: incorrect point, got %v want %v", xx.Start, want.Range.Start)
+ if got.Start != want.Range.Start {
+ t.Errorf("prepare rename failed: incorrect point, got %v want %v", got.Start, want.Range.Start)
}
} else {
- if protocol.CompareRange(*xx, want.Range) != 0 {
- t.Errorf("prepare rename failed: incorrect range got %v want %v", *xx, want.Range)
+ if protocol.CompareRange(*got, want.Range) != 0 {
+ t.Errorf("prepare rename failed: incorrect range got %v want %v", *got, want.Range)
}
}
}
@@ -701,7 +750,7 @@
func applyWorkspaceEdits(r *runner, wedit protocol.WorkspaceEdit) (map[span.URI]string, error) {
res := map[span.URI]string{}
for _, docEdits := range wedit.DocumentChanges {
- uri := span.URI(docEdits.TextDocument.URI)
+ uri := docEdits.TextDocument.URI.SpanURI()
m, err := r.data.Mapper(uri)
if err != nil {
return nil, err
@@ -734,7 +783,7 @@
func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
params := &protocol.DocumentSymbolParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: string(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
}
symbols, err := r.server.DocumentSymbol(r.ctx, params)
@@ -745,55 +794,80 @@
t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols))
return
}
- if diff := r.diffSymbols(t, uri, expectedSymbols, symbols); diff != "" {
+ if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" {
t.Error(diff)
}
}
-func (r *runner) diffSymbols(t *testing.T, uri span.URI, want []protocol.DocumentSymbol, got []protocol.DocumentSymbol) string {
- sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name })
- sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name })
- if len(got) != len(want) {
- return summarizeSymbols(t, -1, want, got, "different lengths got %v want %v", len(got), len(want))
+func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
+ got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
+ opts.Matcher = source.CaseInsensitive
+ })
+ got = tests.FilterWorkspaceSymbols(got, dirs)
+ if len(got) != len(expectedSymbols) {
+ t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
+ return
}
- for i, w := range want {
- g := got[i]
- if w.Name != g.Name {
- return summarizeSymbols(t, i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
- }
- if w.Kind != g.Kind {
- return summarizeSymbols(t, i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
- }
- if protocol.CompareRange(g.SelectionRange, w.SelectionRange) != 0 {
- return summarizeSymbols(t, i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange)
- }
- if msg := r.diffSymbols(t, uri, w.Children, g.Children); msg != "" {
- return fmt.Sprintf("children of %s: %s", w.Name, msg)
- }
+ if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
+ t.Error(diff)
}
- return ""
}
-func summarizeSymbols(t *testing.T, i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
- msg := &bytes.Buffer{}
- fmt.Fprint(msg, "document symbols failed")
- if i >= 0 {
- fmt.Fprintf(msg, " at %d", i)
+func (r *runner) FuzzyWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
+ got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
+ opts.Matcher = source.Fuzzy
+ })
+ got = tests.FilterWorkspaceSymbols(got, dirs)
+ if len(got) != len(expectedSymbols) {
+ t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
+ return
}
- fmt.Fprint(msg, " because of ")
- fmt.Fprintf(msg, reason, args...)
- fmt.Fprint(msg, ":\nexpected:\n")
- for _, s := range want {
- fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
+ if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
+ t.Error(diff)
}
- fmt.Fprintf(msg, "got:\n")
- for _, s := range got {
- fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
- }
- return msg.String()
}
-func (r *runner) SignatureHelp(t *testing.T, spn span.Span, expectedSignature *source.SignatureInformation) {
+func (r *runner) CaseSensitiveWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
+ got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
+ opts.Matcher = source.CaseSensitive
+ })
+ got = tests.FilterWorkspaceSymbols(got, dirs)
+ if len(got) != len(expectedSymbols) {
+ t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
+ return
+ }
+ if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
+ t.Error(diff)
+ }
+}
+
+func (r *runner) callWorkspaceSymbols(t *testing.T, query string, options func(*source.Options)) []protocol.SymbolInformation {
+ t.Helper()
+
+ for _, view := range r.server.session.Views() {
+ original := view.Options()
+ modified := original
+ options(&modified)
+ var err error
+ view, err = view.SetOptions(r.ctx, modified)
+ if err != nil {
+ t.Error(err)
+ return nil
+ }
+ defer view.SetOptions(r.ctx, original)
+ }
+
+ params := &protocol.WorkspaceSymbolParams{
+ Query: query,
+ }
+ symbols, err := r.server.Symbol(r.ctx, params)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return symbols
+}
+
+func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) {
m, err := r.data.Mapper(spn.URI())
if err != nil {
t.Fatal(err)
@@ -804,70 +878,35 @@
}
tdpp := protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(spn.URI()),
+ URI: protocol.URIFromSpanURI(spn.URI()),
},
Position: loc.Range.Start,
}
params := &protocol.SignatureHelpParams{
TextDocumentPositionParams: tdpp,
}
- gotSignatures, err := r.server.SignatureHelp(r.ctx, params)
+ got, err := r.server.SignatureHelp(r.ctx, params)
if err != nil {
// Only fail if we got an error we did not expect.
- if expectedSignature != nil {
+ if want != nil {
t.Fatal(err)
}
return
}
- if expectedSignature == nil {
- if gotSignatures != nil {
- t.Errorf("expected no signature, got %v", gotSignatures)
+ if want == nil {
+ if got != nil {
+ t.Errorf("expected no signature, got %v", got)
}
return
}
- if gotSignatures == nil {
- t.Fatalf("expected %v, got nil", expectedSignature)
+ if got == nil {
+ t.Fatalf("expected %v, got nil", want)
}
- if diff := diffSignatures(spn, expectedSignature, gotSignatures); diff != "" {
+ if diff := tests.DiffSignatures(spn, want, got); diff != "" {
t.Error(diff)
}
}
-func diffSignatures(spn span.Span, want *source.SignatureInformation, got *protocol.SignatureHelp) string {
- decorate := func(f string, args ...interface{}) string {
- return fmt.Sprintf("Invalid signature at %s: %s", spn, fmt.Sprintf(f, args...))
- }
-
- if len(got.Signatures) != 1 {
- return decorate("wanted 1 signature, got %d", len(got.Signatures))
- }
-
- if got.ActiveSignature != 0 {
- return decorate("wanted active signature of 0, got %d", got.ActiveSignature)
- }
-
- if want.ActiveParameter != int(got.ActiveParameter) {
- return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, got.ActiveParameter)
- }
-
- gotSig := got.Signatures[int(got.ActiveSignature)]
-
- if want.Label != gotSig.Label {
- return decorate("wanted label %q, got %q", want.Label, gotSig.Label)
- }
-
- var paramParts []string
- for _, p := range gotSig.Parameters {
- paramParts = append(paramParts, p.Label)
- }
- paramsStr := strings.Join(paramParts, ", ")
- if !strings.Contains(gotSig.Label, paramsStr) {
- return decorate("expected signature %q to contain params %q", gotSig.Label, paramsStr)
- }
-
- return ""
-}
-
func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {
m, err := r.data.Mapper(uri)
if err != nil {
@@ -875,7 +914,7 @@
}
got, err := r.server.DocumentLink(r.ctx, &protocol.DocumentLinkParams{
TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(uri),
},
})
if err != nil {
@@ -913,7 +952,7 @@
fset := token.NewFileSet()
f := fset.AddFile(fname, -1, len(test.text))
f.SetLinesForContent([]byte(test.text))
- uri := span.FileURI(fname)
+ uri := span.URIFromPath(fname)
converter := span.NewContentConverter(fname, []byte(test.text))
mapper := &protocol.ColumnMapper{
URI: uri,
@@ -929,105 +968,3 @@
}
}
}
-
-// TODO(golang/go#36091): This function can be refactored to look like the rest of this file
-// when marker support gets added for go.mod files.
-func TestModfileSuggestedFixes(t *testing.T) {
- if runtime.GOOS == "android" {
- t.Skip("this test cannot find mod/testdata files")
- }
-
- ctx := tests.Context(t)
- cache := cache.New(nil)
- session := cache.NewSession()
- options := tests.DefaultOptions()
- options.TempModfile = true
- options.Env = append(os.Environ(), "GOPACKAGESDRIVER=off", "GOROOT=")
-
- server := Server{
- session: session,
- delivered: map[span.URI]sentDiagnostics{},
- }
-
- for _, tt := range []string{"indirect", "unused"} {
- t.Run(tt, func(t *testing.T) {
- folder, err := tests.CopyFolderToTempDir(filepath.Join("mod", "testdata", tt))
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(folder)
-
- _, snapshot, err := session.NewView(ctx, "suggested_fix_test", span.FileURI(folder), options)
- if err != nil {
- t.Fatal(err)
- }
-
- realURI, tempURI := snapshot.View().ModFiles()
- // TODO: Add testing for when the -modfile flag is turned off and we still get diagnostics.
- if tempURI == "" {
- return
- }
- realfh, err := snapshot.GetFile(realURI)
- if err != nil {
- t.Fatal(err)
- }
-
- reports, err := mod.Diagnostics(ctx, snapshot)
- if err != nil {
- t.Fatal(err)
- }
- if len(reports) != 1 {
- t.Errorf("expected 1 fileHandle, got %d", len(reports))
- }
-
- _, m, _, _, err := snapshot.ModTidyHandle(ctx, realfh).Tidy(ctx)
- if err != nil {
- t.Fatal(err)
- }
-
- for fh, diags := range reports {
- actions, err := server.CodeAction(ctx, &protocol.CodeActionParams{
- TextDocument: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(fh.URI),
- },
- Context: protocol.CodeActionContext{
- Only: []protocol.CodeActionKind{protocol.SourceOrganizeImports},
- Diagnostics: toProtocolDiagnostics(diags),
- },
- })
- if err != nil {
- t.Fatal(err)
- }
- if len(actions) == 0 {
- t.Fatal("no code actions returned")
- }
- if len(actions) > 1 {
- t.Fatal("expected only 1 code action")
- }
- res := map[span.URI]string{}
- for _, docEdits := range actions[0].Edit.DocumentChanges {
- uri := span.URI(docEdits.TextDocument.URI)
- content, err := ioutil.ReadFile(uri.Filename())
- if err != nil {
- t.Fatal(err)
- }
- res[uri] = string(content)
- sedits, err := source.FromProtocolEdits(m, docEdits.Edits)
- if err != nil {
- t.Fatal(err)
- }
- res[uri] = applyEdits(res[uri], sedits)
- }
- got := res[realfh.Identity().URI]
- contents, err := ioutil.ReadFile(filepath.Join(folder, "go.mod.golden"))
- if err != nil {
- t.Fatal(err)
- }
- want := string(contents)
- if want != got {
- t.Errorf("suggested fixes failed for %s, expected:\n%s\ngot:\n%s", fh.URI.Filename(), want, got)
- }
- }
- })
- }
-}
diff --git a/internal/lsp/lsprpc/autostart_posix.go b/internal/lsp/lsprpc/autostart_posix.go
new file mode 100644
index 0000000..99b6606
--- /dev/null
+++ b/internal/lsp/lsprpc/autostart_posix.go
@@ -0,0 +1,49 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !windows
+
+package lsprpc
+
+import (
+ "crypto/sha1"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+// autoNetworkAddress resolves an id on the 'auto' pseduo-network to a
+// real network and address. On unix, this uses unix domain sockets.
+func autoNetworkAddress(goplsPath, id string) (network string, address string) {
+ // Especially when doing local development or testing, it's important that
+ // the remote gopls instance we connect to is running the same binary as our
+ // forwarder. So we encode a short hash of the binary path into the daemon
+ // socket name. If possible, we also include the buildid in this hash, to
+ // account for long-running processes where the binary has been subsequently
+ // rebuilt.
+ h := sha1.New()
+ cmd := exec.Command("go", "tool", "buildid", goplsPath)
+ cmd.Stdout = h
+ var pathHash []byte
+ if err := cmd.Run(); err == nil {
+ pathHash = h.Sum(nil)
+ } else {
+ log.Printf("error getting current buildid: %v", err)
+ sum := sha1.Sum([]byte(goplsPath))
+ pathHash = sum[:]
+ }
+ shortHash := fmt.Sprintf("%x", pathHash)[:6]
+ user := os.Getenv("USER")
+ if user == "" {
+ user = "shared"
+ }
+ basename := filepath.Base(goplsPath)
+ idComponent := ""
+ if id != "" {
+ idComponent = "-" + id
+ }
+ return "unix", filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s-daemon.%s%s", basename, shortHash, user, idComponent))
+}
diff --git a/internal/lsp/lsprpc/autostart_windows.go b/internal/lsp/lsprpc/autostart_windows.go
new file mode 100644
index 0000000..68f9bf8
--- /dev/null
+++ b/internal/lsp/lsprpc/autostart_windows.go
@@ -0,0 +1,17 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build windows
+
+package lsprpc
+
+// autoNetworkAddress returns the default network and address for the
+// automatically-started gopls remote. See autostart_posix.go for more
+// information.
+func autoNetworkAddress(goplsPath, id string) (network string, address string) {
+ if id != "" {
+ panic("identified remotes are not supported on windows")
+ }
+ return "tcp", ":37374"
+}
diff --git a/internal/lsp/lsprpc/lsprpc.go b/internal/lsp/lsprpc/lsprpc.go
new file mode 100644
index 0000000..4b1e737
--- /dev/null
+++ b/internal/lsp/lsprpc/lsprpc.go
@@ -0,0 +1,432 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package lsprpc implements a jsonrpc2.StreamServer that may be used to
+// serve the LSP on a jsonrpc2 channel.
+package lsprpc
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ stdlog "log"
+ "net"
+ "os"
+ "os/exec"
+ "strconv"
+ "sync/atomic"
+ "time"
+
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/tools/internal/jsonrpc2"
+ "golang.org/x/tools/internal/lsp"
+ "golang.org/x/tools/internal/lsp/cache"
+ "golang.org/x/tools/internal/lsp/debug"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/telemetry/log"
+)
+
+// AutoNetwork is the pseudo network type used to signal that gopls should use
+// automatic discovery to resolve a remote address.
+const AutoNetwork = "auto"
+
+// The StreamServer type is a jsonrpc2.StreamServer that handles incoming
+// streams as a new LSP session, using a shared cache.
+type StreamServer struct {
+ withTelemetry bool
+ debug *debug.Instance
+ cache *cache.Cache
+
+ // serverForTest may be set to a test fake for testing.
+ serverForTest protocol.Server
+}
+
+var clientIndex, serverIndex int64
+
+// NewStreamServer creates a StreamServer using the shared cache. If
+// withTelemetry is true, each session is instrumented with telemetry that
+// records RPC statistics.
+func NewStreamServer(cache *cache.Cache, withTelemetry bool, debugInstance *debug.Instance) *StreamServer {
+ s := &StreamServer{
+ withTelemetry: withTelemetry,
+ debug: debugInstance,
+ cache: cache,
+ }
+ return s
+}
+
+// debugInstance is the common functionality shared between client and server
+// gopls instances.
+type debugInstance struct {
+ id string
+ debugAddress string
+ logfile string
+ goplsPath string
+}
+
+func (d debugInstance) ID() string {
+ return d.id
+}
+
+func (d debugInstance) DebugAddress() string {
+ return d.debugAddress
+}
+
+func (d debugInstance) Logfile() string {
+ return d.logfile
+}
+
+func (d debugInstance) GoplsPath() string {
+ return d.goplsPath
+}
+
+// A debugServer is held by the client to identity the remove server to which
+// it is connected.
+type debugServer struct {
+ debugInstance
+ // clientID is the id of this client on the server.
+ clientID string
+}
+
+func (s debugServer) ClientID() string {
+ return s.clientID
+}
+
+// A debugClient is held by the server to identify an incoming client
+// connection.
+type debugClient struct {
+ debugInstance
+ // session is the session serving this client.
+ session *cache.Session
+ // serverID is this id of this server on the client.
+ serverID string
+}
+
+func (c debugClient) Session() debug.Session {
+ return cache.DebugSession{Session: c.session}
+}
+
+func (c debugClient) ServerID() string {
+ return c.serverID
+}
+
+// ServeStream implements the jsonrpc2.StreamServer interface, by handling
+// incoming streams using a new lsp server.
+func (s *StreamServer) ServeStream(ctx context.Context, stream jsonrpc2.Stream) error {
+ index := atomic.AddInt64(&clientIndex, 1)
+
+ conn := jsonrpc2.NewConn(stream)
+ client := protocol.ClientDispatcher(conn)
+ session := s.cache.NewSession()
+ dc := &debugClient{
+ debugInstance: debugInstance{
+ id: strconv.FormatInt(index, 10),
+ },
+ session: session,
+ }
+ s.debug.State.AddClient(dc)
+ defer s.debug.State.DropClient(dc)
+
+ server := s.serverForTest
+ if server == nil {
+ server = lsp.NewServer(session, client)
+ }
+ // Clients may or may not send a shutdown message. Make sure the server is
+ // shut down.
+ // TODO(rFindley): this shutdown should perhaps be on a disconnected context.
+ defer server.Shutdown(ctx)
+ conn.AddHandler(protocol.ServerHandler(server))
+ conn.AddHandler(protocol.Canceller{})
+ if s.withTelemetry {
+ conn.AddHandler(telemetryHandler{})
+ }
+ executable, err := os.Executable()
+ if err != nil {
+ stdlog.Printf("error getting gopls path: %v", err)
+ executable = ""
+ }
+ conn.AddHandler(&handshaker{
+ client: dc,
+ debug: s.debug,
+ goplsPath: executable,
+ })
+ return conn.Run(protocol.WithClient(ctx, client))
+}
+
+// A Forwarder is a jsonrpc2.StreamServer that handles an LSP stream by
+// forwarding it to a remote. This is used when the gopls process started by
+// the editor is in the `-remote` mode, which means it finds and connects to a
+// separate gopls daemon. In these cases, we still want the forwarder gopls to
+// be instrumented with telemetry, and want to be able to in some cases hijack
+// the jsonrpc2 connection with the daemon.
+type Forwarder struct {
+ network, addr string
+
+ // Configuration. Right now, not all of this may be customizable, but in the
+ // future it probably will be.
+ withTelemetry bool
+ dialTimeout time.Duration
+ retries int
+ debug *debug.Instance
+ goplsPath string
+}
+
+// NewForwarder creates a new Forwarder, ready to forward connections to the
+// remote server specified by network and addr.
+func NewForwarder(network, addr string, withTelemetry bool, debugInstance *debug.Instance) *Forwarder {
+ gp, err := os.Executable()
+ if err != nil {
+ stdlog.Printf("error getting gopls path for forwarder: %v", err)
+ gp = ""
+ }
+
+ return &Forwarder{
+ network: network,
+ addr: addr,
+ withTelemetry: withTelemetry,
+ dialTimeout: 1 * time.Second,
+ retries: 5,
+ debug: debugInstance,
+ goplsPath: gp,
+ }
+}
+
+// ServeStream dials the forwarder remote and binds the remote to serve the LSP
+// on the incoming stream.
+func (f *Forwarder) ServeStream(ctx context.Context, stream jsonrpc2.Stream) error {
+ clientConn := jsonrpc2.NewConn(stream)
+ client := protocol.ClientDispatcher(clientConn)
+
+ netConn, err := f.connectToRemote(ctx)
+ if err != nil {
+ return fmt.Errorf("forwarder: connecting to remote: %v", err)
+ }
+ serverConn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn, netConn))
+ server := protocol.ServerDispatcher(serverConn)
+
+ // Forward between connections.
+ serverConn.AddHandler(protocol.ClientHandler(client))
+ serverConn.AddHandler(protocol.Canceller{})
+ clientConn.AddHandler(protocol.ServerHandler(server))
+ clientConn.AddHandler(protocol.Canceller{})
+ clientConn.AddHandler(forwarderHandler{})
+ if f.withTelemetry {
+ clientConn.AddHandler(telemetryHandler{})
+ }
+ g, ctx := errgroup.WithContext(ctx)
+ g.Go(func() error {
+ return serverConn.Run(ctx)
+ })
+ // Don't run the clientConn yet, so that we can complete the handshake before
+ // processing any client messages.
+
+ // Do a handshake with the server instance to exchange debug information.
+ index := atomic.AddInt64(&serverIndex, 1)
+ serverID := strconv.FormatInt(index, 10)
+ var (
+ hreq = handshakeRequest{
+ ServerID: serverID,
+ Logfile: f.debug.Logfile,
+ DebugAddr: f.debug.ListenedDebugAddress,
+ GoplsPath: f.goplsPath,
+ }
+ hresp handshakeResponse
+ )
+ if err := serverConn.Call(ctx, handshakeMethod, hreq, &hresp); err != nil {
+ log.Error(ctx, "forwarder: gopls handshake failed", err)
+ }
+ if hresp.GoplsPath != f.goplsPath {
+ log.Error(ctx, "", fmt.Errorf("forwarder: gopls path mismatch: forwarder is %q, remote is %q", f.goplsPath, hresp.GoplsPath))
+ }
+ f.debug.State.AddServer(debugServer{
+ debugInstance: debugInstance{
+ id: serverID,
+ logfile: hresp.Logfile,
+ debugAddress: hresp.DebugAddr,
+ goplsPath: hresp.GoplsPath,
+ },
+ clientID: hresp.ClientID,
+ })
+ g.Go(func() error {
+ return clientConn.Run(ctx)
+ })
+
+ return g.Wait()
+}
+
+func (f *Forwarder) connectToRemote(ctx context.Context) (net.Conn, error) {
+ var (
+ netConn net.Conn
+ err error
+ network, address = f.network, f.addr
+ )
+ if f.network == AutoNetwork {
+ // f.network is overloaded to support a concept of 'automatic' addresses,
+ // which signals that the gopls remote address should be automatically
+ // derived.
+ // So we need to resolve a real network and address here.
+ network, address = autoNetworkAddress(f.goplsPath, f.addr)
+ }
+ // Try dialing our remote once, in case it is already running.
+ netConn, err = net.DialTimeout(network, address, f.dialTimeout)
+ if err == nil {
+ return netConn, nil
+ }
+ // If our remote is on the 'auto' network, start it if it doesn't exist.
+ if f.network == AutoNetwork {
+ if f.goplsPath == "" {
+ return nil, fmt.Errorf("cannot auto-start remote: gopls path is unknown")
+ }
+ if network == "unix" {
+ // Sometimes the socketfile isn't properly cleaned up when gopls shuts
+ // down. Since we have already tried and failed to dial this address, it
+ // should *usually* be safe to remove the socket before binding to the
+ // address.
+ // TODO(rfindley): there is probably a race here if multiple gopls
+ // instances are simultaneously starting up.
+ if _, err := os.Stat(address); err == nil {
+ if err := os.Remove(address); err != nil {
+ return nil, fmt.Errorf("removing remote socket file: %v", err)
+ }
+ }
+ }
+ if err := startRemote(f.goplsPath, network, address); err != nil {
+ return nil, fmt.Errorf("startRemote(%q, %q): %v", network, address, err)
+ }
+ }
+
+ // It can take some time for the newly started server to bind to our address,
+ // so we retry for a bit.
+ for retry := 0; retry < f.retries; retry++ {
+ startDial := time.Now()
+ netConn, err = net.DialTimeout(network, address, f.dialTimeout)
+ if err == nil {
+ return netConn, nil
+ }
+ log.Print(ctx, fmt.Sprintf("failed attempt #%d to connect to remote: %v\n", retry+2, err))
+ // In case our failure was a fast-failure, ensure we wait at least
+ // f.dialTimeout before trying again.
+ if retry != f.retries-1 {
+ time.Sleep(f.dialTimeout - time.Since(startDial))
+ }
+ }
+ return nil, fmt.Errorf("dialing remote: %v", err)
+}
+
+func startRemote(goplsPath, network, address string) error {
+ args := []string{"serve",
+ "-listen", fmt.Sprintf(`%s;%s`, network, address),
+ "-listen.timeout", "1m",
+ "-debug", ":0",
+ "-logfile", "auto",
+ }
+ cmd := exec.Command(goplsPath, args...)
+ if err := cmd.Start(); err != nil {
+ return fmt.Errorf("starting remote gopls: %v", err)
+ }
+ return nil
+}
+
+// ForwarderExitFunc is used to exit the forwarder process. It is mutable for
+// testing purposes.
+var ForwarderExitFunc = os.Exit
+
+// OverrideExitFuncsForTest can be used from test code to prevent the test
+// process from exiting on server shutdown. The returned func reverts the exit
+// funcs to their previous state.
+func OverrideExitFuncsForTest() func() {
+ // Override functions that would shut down the test process
+ cleanup := func(lspExit, forwarderExit func(code int)) func() {
+ return func() {
+ lsp.ServerExitFunc = lspExit
+ ForwarderExitFunc = forwarderExit
+ }
+ }(lsp.ServerExitFunc, ForwarderExitFunc)
+ // It is an error for a test to shutdown a server process.
+ lsp.ServerExitFunc = func(code int) {
+ panic(fmt.Sprintf("LSP server exited with code %d", code))
+ }
+ // We don't want our forwarders to exit, but it's OK if they would have.
+ ForwarderExitFunc = func(code int) {}
+ return cleanup
+}
+
+// forwarderHandler intercepts 'exit' messages to prevent the shared gopls
+// instance from exiting. In the future it may also intercept 'shutdown' to
+// provide more graceful shutdown of the client connection.
+type forwarderHandler struct {
+ jsonrpc2.EmptyHandler
+}
+
+func (forwarderHandler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
+ // TODO(golang.org/issues/34111): we should more gracefully disconnect here,
+ // once that process exists.
+ if r.Method == "exit" {
+ ForwarderExitFunc(0)
+ // Still return true here to prevent the message from being delivered: in
+ // tests, ForwarderExitFunc may be overridden to something that doesn't
+ // exit the process.
+ return true
+ }
+ return false
+}
+
+type handshaker struct {
+ jsonrpc2.EmptyHandler
+ client *debugClient
+ debug *debug.Instance
+ goplsPath string
+}
+
+type handshakeRequest struct {
+ ServerID string `json:"serverID"`
+ Logfile string `json:"logfile"`
+ DebugAddr string `json:"debugAddr"`
+ GoplsPath string `json:"goplsPath"`
+}
+
+type handshakeResponse struct {
+ ClientID string `json:"clientID"`
+ SessionID string `json:"sessionID"`
+ Logfile string `json:"logfile"`
+ DebugAddr string `json:"debugAddr"`
+ GoplsPath string `json:"goplsPath"`
+}
+
+const handshakeMethod = "gopls/handshake"
+
+func (h *handshaker) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
+ if r.Method == handshakeMethod {
+ var req handshakeRequest
+ if err := json.Unmarshal(*r.Params, &req); err != nil {
+ sendError(ctx, r, err)
+ return true
+ }
+ h.client.debugAddress = req.DebugAddr
+ h.client.logfile = req.Logfile
+ h.client.serverID = req.ServerID
+ h.client.goplsPath = req.GoplsPath
+ resp := handshakeResponse{
+ ClientID: h.client.id,
+ SessionID: cache.DebugSession{Session: h.client.session}.ID(),
+ Logfile: h.debug.Logfile,
+ DebugAddr: h.debug.ListenedDebugAddress,
+ GoplsPath: h.goplsPath,
+ }
+ if err := r.Reply(ctx, resp, nil); err != nil {
+ log.Error(ctx, "replying to handshake", err)
+ }
+ return true
+ }
+ return false
+}
+
+func sendError(ctx context.Context, req *jsonrpc2.Request, err error) {
+ if _, ok := err.(*jsonrpc2.Error); !ok {
+ err = jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err)
+ }
+ if err := req.Reply(ctx, nil, err); err != nil {
+ log.Error(ctx, "", err)
+ }
+}
diff --git a/internal/lsp/lsprpc/lsprpc_test.go b/internal/lsp/lsprpc/lsprpc_test.go
new file mode 100644
index 0000000..1bca640
--- /dev/null
+++ b/internal/lsp/lsprpc/lsprpc_test.go
@@ -0,0 +1,231 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package lsprpc
+
+import (
+ "context"
+ "regexp"
+ "sync"
+ "testing"
+ "time"
+
+ "golang.org/x/tools/internal/jsonrpc2/servertest"
+ "golang.org/x/tools/internal/lsp/cache"
+ "golang.org/x/tools/internal/lsp/debug"
+ "golang.org/x/tools/internal/lsp/fake"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/telemetry/log"
+)
+
+type fakeClient struct {
+ protocol.Client
+
+ logs chan string
+}
+
+func (c fakeClient) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error {
+ c.logs <- params.Message
+ return nil
+}
+
+type pingServer struct{ protocol.Server }
+
+func (s pingServer) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
+ log.Print(ctx, "ping")
+ return nil
+}
+
+func (s pingServer) Shutdown(ctx context.Context) error {
+ return nil
+}
+
+func TestClientLogging(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ server := pingServer{}
+ client := fakeClient{logs: make(chan string, 10)}
+
+ di := debug.NewInstance("", "")
+ ss := NewStreamServer(cache.New(nil, di.State), false, di)
+ ss.serverForTest = server
+ ts := servertest.NewPipeServer(ctx, ss)
+ defer ts.Close()
+ cc := ts.Connect(ctx)
+ cc.AddHandler(protocol.ClientHandler(client))
+
+ protocol.ServerDispatcher(cc).DidOpen(ctx, &protocol.DidOpenTextDocumentParams{})
+
+ select {
+ case got := <-client.logs:
+ want := "ping"
+ matched, err := regexp.MatchString(want, got)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !matched {
+ t.Errorf("got log %q, want a log containing %q", got, want)
+ }
+ case <-time.After(1 * time.Second):
+ t.Error("timeout waiting for client log")
+ }
+}
+
+// waitableServer instruments LSP request so that we can control their timing.
+// The requests chosen are arbitrary: we simply needed one that blocks, and
+// another that doesn't.
+type waitableServer struct {
+ protocol.Server
+
+ started chan struct{}
+}
+
+func (s waitableServer) Hover(ctx context.Context, _ *protocol.HoverParams) (*protocol.Hover, error) {
+ s.started <- struct{}{}
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-time.After(200 * time.Millisecond):
+ }
+ return &protocol.Hover{}, nil
+}
+
+func (s waitableServer) Resolve(_ context.Context, item *protocol.CompletionItem) (*protocol.CompletionItem, error) {
+ return item, nil
+}
+
+func (s waitableServer) Shutdown(ctx context.Context) error {
+ return nil
+}
+
+func TestRequestCancellation(t *testing.T) {
+ server := waitableServer{
+ started: make(chan struct{}),
+ }
+ diserve := debug.NewInstance("", "")
+ ss := NewStreamServer(cache.New(nil, diserve.State), false, diserve)
+ ss.serverForTest = server
+ ctx := context.Background()
+ tsDirect := servertest.NewTCPServer(ctx, ss)
+ defer tsDirect.Close()
+
+ forwarder := NewForwarder("tcp", tsDirect.Addr, false, debug.NewInstance("", ""))
+ tsForwarded := servertest.NewPipeServer(ctx, forwarder)
+ defer tsForwarded.Close()
+
+ tests := []struct {
+ serverType string
+ ts servertest.Connector
+ }{
+ {"direct", tsDirect},
+ {"forwarder", tsForwarded},
+ }
+
+ for _, test := range tests {
+ t.Run(test.serverType, func(t *testing.T) {
+ cc := test.ts.Connect(ctx)
+ cc.AddHandler(protocol.Canceller{})
+ ctx := context.Background()
+ ctx1, cancel1 := context.WithCancel(ctx)
+ var (
+ err1, err2 error
+ wg sync.WaitGroup
+ )
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ _, err1 = protocol.ServerDispatcher(cc).Hover(ctx1, &protocol.HoverParams{})
+ }()
+ go func() {
+ defer wg.Done()
+ _, err2 = protocol.ServerDispatcher(cc).Resolve(ctx, &protocol.CompletionItem{})
+ }()
+ // Wait for the Hover request to start.
+ <-server.started
+ cancel1()
+ wg.Wait()
+ if err1 == nil {
+ t.Errorf("cancelled Hover(): got nil err")
+ }
+ if err2 != nil {
+ t.Errorf("uncancelled Hover(): err: %v", err2)
+ }
+ if _, err := protocol.ServerDispatcher(cc).Resolve(ctx, &protocol.CompletionItem{}); err != nil {
+ t.Errorf("subsequent Hover(): %v", err)
+ }
+ })
+ }
+}
+
+const exampleProgram = `
+-- go.mod --
+module mod
+
+go 1.12
+-- main.go --
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello World.")
+}`
+
+func TestDebugInfoLifecycle(t *testing.T) {
+ resetExitFuncs := OverrideExitFuncsForTest()
+ defer resetExitFuncs()
+
+ clientDebug := debug.NewInstance("", "")
+ serverDebug := debug.NewInstance("", "")
+
+ cache := cache.New(nil, serverDebug.State)
+ ss := NewStreamServer(cache, false, serverDebug)
+ ctx := context.Background()
+ tsBackend := servertest.NewTCPServer(ctx, ss)
+
+ forwarder := NewForwarder("tcp", tsBackend.Addr, false, clientDebug)
+ tsForwarder := servertest.NewPipeServer(ctx, forwarder)
+
+ ws, err := fake.NewWorkspace("gopls-lsprpc-test", []byte(exampleProgram))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ws.Close()
+
+ conn1 := tsForwarder.Connect(ctx)
+ ed1, err := fake.NewConnectedEditor(ctx, ws, conn1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ed1.Shutdown(ctx)
+ conn2 := tsBackend.Connect(ctx)
+ ed2, err := fake.NewConnectedEditor(ctx, ws, conn2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ed2.Shutdown(ctx)
+
+ if got, want := len(serverDebug.State.Clients()), 2; got != want {
+ t.Errorf("len(server:Clients) = %d, want %d", got, want)
+ }
+ if got, want := len(serverDebug.State.Sessions()), 2; got != want {
+ t.Errorf("len(server:Sessions) = %d, want %d", got, want)
+ }
+ if got, want := len(clientDebug.State.Servers()), 1; got != want {
+ t.Errorf("len(client:Servers) = %d, want %d", got, want)
+ }
+ // Close one of the connections to verify that the client and session were
+ // dropped.
+ if err := ed1.Shutdown(ctx); err != nil {
+ t.Fatal(err)
+ }
+ if got, want := len(serverDebug.State.Sessions()), 1; got != want {
+ t.Errorf("len(server:Sessions()) = %d, want %d", got, want)
+ }
+ // TODO(rfindley): once disconnection works, assert that len(Clients) == 1
+ // (as of writing, it is still 2)
+}
+
+// TODO: add a test for telemetry.
diff --git a/internal/lsp/lsprpc/telemetry.go b/internal/lsp/lsprpc/telemetry.go
new file mode 100644
index 0000000..6909261
--- /dev/null
+++ b/internal/lsp/lsprpc/telemetry.go
@@ -0,0 +1,115 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package lsprpc
+
+import (
+ "context"
+ "encoding/json"
+ "time"
+
+ "golang.org/x/tools/internal/jsonrpc2"
+ "golang.org/x/tools/internal/lsp/telemetry"
+ "golang.org/x/tools/internal/telemetry/trace"
+)
+
+type telemetryHandler struct{}
+
+func (h telemetryHandler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
+ stats := h.getStats(ctx)
+ if stats != nil {
+ stats.delivering()
+ }
+ return false
+}
+
+func (h telemetryHandler) Cancel(ctx context.Context, conn *jsonrpc2.Conn, id jsonrpc2.ID, cancelled bool) bool {
+ return false
+}
+
+func (h telemetryHandler) Request(ctx context.Context, conn *jsonrpc2.Conn, direction jsonrpc2.Direction, r *jsonrpc2.WireRequest) context.Context {
+ if r.Method == "" {
+ panic("no method in rpc stats")
+ }
+ stats := &rpcStats{
+ method: r.Method,
+ start: time.Now(),
+ direction: direction,
+ payload: r.Params,
+ }
+ ctx = context.WithValue(ctx, statsKey, stats)
+ mode := telemetry.Outbound
+ if direction == jsonrpc2.Receive {
+ mode = telemetry.Inbound
+ }
+ ctx, stats.close = trace.StartSpan(ctx, r.Method,
+ telemetry.Method.Of(r.Method),
+ telemetry.RPCDirection.Of(mode),
+ telemetry.RPCID.Of(r.ID),
+ )
+ telemetry.Started.Record(ctx, 1)
+ _, stats.delivering = trace.StartSpan(ctx, "queued")
+ return ctx
+}
+
+func (h telemetryHandler) Response(ctx context.Context, conn *jsonrpc2.Conn, direction jsonrpc2.Direction, r *jsonrpc2.WireResponse) context.Context {
+ return ctx
+}
+
+func (h telemetryHandler) Done(ctx context.Context, err error) {
+ stats := h.getStats(ctx)
+ if err != nil {
+ ctx = telemetry.StatusCode.With(ctx, "ERROR")
+ } else {
+ ctx = telemetry.StatusCode.With(ctx, "OK")
+ }
+ elapsedTime := time.Since(stats.start)
+ latencyMillis := float64(elapsedTime) / float64(time.Millisecond)
+ telemetry.Latency.Record(ctx, latencyMillis)
+ stats.close()
+}
+
+func (h telemetryHandler) Read(ctx context.Context, bytes int64) context.Context {
+ telemetry.SentBytes.Record(ctx, bytes)
+ return ctx
+}
+
+func (h telemetryHandler) Wrote(ctx context.Context, bytes int64) context.Context {
+ telemetry.ReceivedBytes.Record(ctx, bytes)
+ return ctx
+}
+
+const eol = "\r\n\r\n\r\n"
+
+func (h telemetryHandler) Error(ctx context.Context, err error) {
+}
+
+func (h telemetryHandler) getStats(ctx context.Context) *rpcStats {
+ stats, ok := ctx.Value(statsKey).(*rpcStats)
+ if !ok || stats == nil {
+ method, ok := ctx.Value(telemetry.Method).(string)
+ if !ok {
+ method = "???"
+ }
+ stats = &rpcStats{
+ method: method,
+ close: func() {},
+ }
+ }
+ return stats
+}
+
+type rpcStats struct {
+ method string
+ direction jsonrpc2.Direction
+ id *jsonrpc2.ID
+ payload *json.RawMessage
+ start time.Time
+ delivering func()
+ close func()
+}
+
+type statsKeyType int
+
+const statsKey = statsKeyType(0)
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
new file mode 100644
index 0000000..bb08788
--- /dev/null
+++ b/internal/lsp/mod/code_lens.go
@@ -0,0 +1,67 @@
+package mod
+
+import (
+ "context"
+ "fmt"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/lsp/telemetry"
+ "golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/telemetry/trace"
+)
+
+func CodeLens(ctx context.Context, snapshot source.Snapshot, uri span.URI) ([]protocol.CodeLens, error) {
+ realURI, _ := snapshot.View().ModFiles()
+ if realURI == "" {
+ return nil, nil
+ }
+ // Only get code lens on the go.mod for the view.
+ if uri != realURI {
+ return nil, nil
+ }
+ ctx, done := trace.StartSpan(ctx, "mod.CodeLens", telemetry.File.Of(realURI))
+ defer done()
+
+ fh, err := snapshot.GetFile(realURI)
+ if err != nil {
+ return nil, err
+ }
+ f, m, upgrades, err := snapshot.ModHandle(ctx, fh).Upgrades(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var codelens []protocol.CodeLens
+ for _, req := range f.Require {
+ dep := req.Mod.Path
+ latest, ok := upgrades[dep]
+ if !ok {
+ continue
+ }
+ // Get the range of the require directive.
+ s, e := req.Syntax.Start, req.Syntax.End
+ line, col, err := m.Converter.ToPosition(s.Byte)
+ if err != nil {
+ return nil, err
+ }
+ start := span.NewPoint(line, col, s.Byte)
+ line, col, err = m.Converter.ToPosition(e.Byte)
+ if err != nil {
+ return nil, err
+ }
+ end := span.NewPoint(line, col, e.Byte)
+ rng, err := m.Range(span.New(uri, start, end))
+ if err != nil {
+ return nil, err
+ }
+ codelens = append(codelens, protocol.CodeLens{
+ Range: rng,
+ Command: protocol.Command{
+ Title: fmt.Sprintf("Upgrade dependency to %s", latest),
+ Command: "upgrade.dependency",
+ Arguments: []interface{}{uri, dep},
+ },
+ })
+ }
+ return codelens, err
+}
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index 960f493..401f9ef 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -8,7 +8,10 @@
import (
"context"
+ "fmt"
+ "regexp"
+ "golang.org/x/mod/modfile"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
@@ -16,13 +19,13 @@
"golang.org/x/tools/internal/telemetry/trace"
)
-func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.FileIdentity][]source.Diagnostic, error) {
+func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.FileIdentity][]source.Diagnostic, map[string]*modfile.Require, error) {
// TODO: We will want to support diagnostics for go.mod files even when the -modfile flag is turned off.
realURI, tempURI := snapshot.View().ModFiles()
// Check the case when the tempModfile flag is turned off.
if realURI == "" || tempURI == "" {
- return nil, nil
+ return nil, nil, nil
}
ctx, done := trace.StartSpan(ctx, "mod.Diagnostics", telemetry.File.Of(realURI))
@@ -30,11 +33,15 @@
realfh, err := snapshot.GetFile(realURI)
if err != nil {
- return nil, err
+ return nil, nil, err
}
- _, _, _, parseErrors, err := snapshot.ModTidyHandle(ctx, realfh).Tidy(ctx)
+ mth, err := snapshot.ModTidyHandle(ctx, realfh)
if err != nil {
- return nil, err
+ return nil, nil, err
+ }
+ _, _, missingDeps, parseErrors, err := mth.Tidy(ctx)
+ if err != nil {
+ return nil, nil, err
}
reports := map[source.FileIdentity][]source.Diagnostic{
@@ -54,11 +61,15 @@
}
reports[realfh.Identity()] = append(reports[realfh.Identity()], diag)
}
- return reports, nil
+ return reports, missingDeps, nil
}
func SuggestedFixes(ctx context.Context, snapshot source.Snapshot, realfh source.FileHandle, diags []protocol.Diagnostic) []protocol.CodeAction {
- _, _, _, parseErrors, err := snapshot.ModTidyHandle(ctx, realfh).Tidy(ctx)
+ mth, err := snapshot.ModTidyHandle(ctx, realfh)
+ if err != nil {
+ return nil
+ }
+ _, _, _, parseErrors, err := mth.Tidy(ctx)
if err != nil {
return nil
}
@@ -94,7 +105,7 @@
TextDocument: protocol.VersionedTextDocumentIdentifier{
Version: fh.Identity().Version,
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
- URI: protocol.NewURI(fh.Identity().URI),
+ URI: protocol.URIFromSpanURI(fh.Identity().URI),
},
},
Edits: edits,
@@ -107,6 +118,89 @@
return actions
}
+func SuggestedGoFixes(ctx context.Context, snapshot source.Snapshot, gofh source.FileHandle, diags []protocol.Diagnostic) ([]protocol.CodeAction, error) {
+ // TODO: We will want to support diagnostics for go.mod files even when the -modfile flag is turned off.
+ realURI, tempURI := snapshot.View().ModFiles()
+
+ // Check the case when the tempModfile flag is turned off.
+ if realURI == "" || tempURI == "" {
+ return nil, nil
+ }
+
+ ctx, done := trace.StartSpan(ctx, "mod.SuggestedGoFixes", telemetry.File.Of(realURI))
+ defer done()
+
+ realfh, err := snapshot.GetFile(realURI)
+ if err != nil {
+ return nil, err
+ }
+ mth, err := snapshot.ModTidyHandle(ctx, realfh)
+ if err != nil {
+ return nil, err
+ }
+ realFile, realMapper, missingDeps, _, err := mth.Tidy(ctx)
+ if err != nil {
+ return nil, err
+ }
+ // Get the contents of the go.mod file before we make any changes.
+ oldContents, _, err := realfh.Read(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ var actions []protocol.CodeAction
+ for _, diag := range diags {
+ re := regexp.MustCompile(`(.+) is not in your go.mod file`)
+ matches := re.FindStringSubmatch(diag.Message)
+ if len(matches) != 2 {
+ continue
+ }
+ req := missingDeps[matches[1]]
+ if req == nil {
+ continue
+ }
+ // Calculate the quick fix edits that need to be made to the go.mod file.
+ if err := realFile.AddRequire(req.Mod.Path, req.Mod.Version); err != nil {
+ return nil, err
+ }
+ realFile.Cleanup()
+ newContents, err := realFile.Format()
+ if err != nil {
+ return nil, err
+ }
+ // Reset the *modfile.File back to before we added the dependency.
+ if err := realFile.DropRequire(req.Mod.Path); err != nil {
+ return nil, err
+ }
+ // Calculate the edits to be made due to the change.
+ diff := snapshot.View().Options().ComputeEdits(realfh.Identity().URI, string(oldContents), string(newContents))
+ edits, err := source.ToProtocolEdits(realMapper, diff)
+ if err != nil {
+ return nil, err
+ }
+ action := protocol.CodeAction{
+ Title: fmt.Sprintf("Add %s to go.mod", req.Mod.Path),
+ Kind: protocol.QuickFix,
+ Diagnostics: []protocol.Diagnostic{diag},
+ Edit: protocol.WorkspaceEdit{
+ DocumentChanges: []protocol.TextDocumentEdit{
+ {
+ TextDocument: protocol.VersionedTextDocumentIdentifier{
+ Version: realfh.Identity().Version,
+ TextDocumentIdentifier: protocol.TextDocumentIdentifier{
+ URI: protocol.URIFromSpanURI(realfh.Identity().URI),
+ },
+ },
+ Edits: edits,
+ },
+ },
+ },
+ }
+ actions = append(actions, action)
+ }
+ return actions, nil
+}
+
func sameDiagnostic(d protocol.Diagnostic, e source.Error) bool {
return d.Message == e.Message && protocol.CompareRange(d.Range, e.Range) == 0 && d.Source == e.Category
}
diff --git a/internal/lsp/mod/hover.go b/internal/lsp/mod/hover.go
new file mode 100644
index 0000000..a6a79b9
--- /dev/null
+++ b/internal/lsp/mod/hover.go
@@ -0,0 +1,149 @@
+package mod
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "go/token"
+ "strings"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/telemetry/trace"
+)
+
+func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
+ realURI, _ := snapshot.View().ModFiles()
+ // Only get hover information on the go.mod for the view.
+ if realURI == "" || fh.Identity().URI != realURI {
+ return nil, nil
+ }
+ ctx, done := trace.StartSpan(ctx, "mod.Hover")
+ defer done()
+
+ file, m, why, err := snapshot.ModHandle(ctx, fh).Why(ctx)
+ if err != nil {
+ return nil, err
+ }
+ // Get the position of the cursor.
+ spn, err := m.PointSpan(position)
+ if err != nil {
+ return nil, err
+ }
+ hoverRng, err := spn.Range(m.Converter)
+ if err != nil {
+ return nil, err
+ }
+
+ var req *modfile.Require
+ var startPos, endPos int
+ for _, r := range file.Require {
+ dep := []byte(r.Mod.Path)
+ s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte
+ i := bytes.Index(m.Content[s:e], dep)
+ if i == -1 {
+ continue
+ }
+ // Shift the start position to the location of the
+ // dependency within the require statement.
+ startPos, endPos = s+i, s+i+len(dep)
+ if token.Pos(startPos) <= hoverRng.Start && hoverRng.Start <= token.Pos(endPos) {
+ req = r
+ break
+ }
+ }
+ if req == nil || why == nil {
+ return nil, nil
+ }
+ explanation, ok := why[req.Mod.Path]
+ if !ok {
+ return nil, nil
+ }
+ // Get the range to highlight for the hover.
+ line, col, err := m.Converter.ToPosition(startPos)
+ if err != nil {
+ return nil, err
+ }
+ start := span.NewPoint(line, col, startPos)
+
+ line, col, err = m.Converter.ToPosition(endPos)
+ if err != nil {
+ return nil, err
+ }
+ end := span.NewPoint(line, col, endPos)
+
+ spn = span.New(fh.Identity().URI, start, end)
+ rng, err := m.Range(spn)
+ if err != nil {
+ return nil, err
+ }
+ options := snapshot.View().Options()
+ explanation = formatExplanation(explanation, req, options)
+ return &protocol.Hover{
+ Contents: protocol.MarkupContent{
+ Kind: options.PreferredContentFormat,
+ Value: explanation,
+ },
+ Range: rng,
+ }, nil
+}
+
+func formatExplanation(text string, req *modfile.Require, options source.Options) string {
+ text = strings.TrimSuffix(text, "\n")
+ splt := strings.Split(text, "\n")
+ length := len(splt)
+
+ var b strings.Builder
+ // Write the heading as an H3.
+ b.WriteString("##" + splt[0])
+ if options.PreferredContentFormat == protocol.Markdown {
+ b.WriteString("\n\n")
+ } else {
+ b.WriteRune('\n')
+ }
+
+ // If the explanation is 2 lines, then it is of the form:
+ // # golang.org/x/text/encoding
+ // (main module does not need package golang.org/x/text/encoding)
+ if length == 2 {
+ b.WriteString(splt[1])
+ return b.String()
+ }
+
+ imp := splt[length-1]
+ target := imp
+ if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
+ target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
+ }
+ target = fmt.Sprintf("https://%s/%s", options.LinkTarget, target)
+
+ b.WriteString("This module is necessary because ")
+ msg := fmt.Sprintf("[%s](%s) is imported in", imp, target)
+ b.WriteString(msg)
+
+ // If the explanation is 3 lines, then it is of the form:
+ // # golang.org/x/tools
+ // modtest
+ // golang.org/x/tools/go/packages
+ if length == 3 {
+ msg := fmt.Sprintf(" `%s`.", splt[1])
+ b.WriteString(msg)
+ return b.String()
+ }
+
+ // If the explanation is more than 3 lines, then it is of the form:
+ // # golang.org/x/text/language
+ // rsc.io/quote
+ // rsc.io/sampler
+ // golang.org/x/text/language
+ b.WriteString(":\n```text")
+ dash := ""
+ for _, imp := range splt[1 : length-1] {
+ dash += "-"
+ b.WriteString("\n" + dash + " " + imp)
+ }
+ b.WriteString("\n```")
+ return b.String()
+}
diff --git a/internal/lsp/mod/mod_test.go b/internal/lsp/mod/mod_test.go
index 7a149c1..ada211c 100644
--- a/internal/lsp/mod/mod_test.go
+++ b/internal/lsp/mod/mod_test.go
@@ -5,15 +5,12 @@
package mod
import (
- "context"
"io/ioutil"
"os"
"path/filepath"
"testing"
"golang.org/x/tools/internal/lsp/cache"
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/tests"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/testenv"
@@ -24,11 +21,9 @@
os.Exit(m.Run())
}
-// TODO(golang/go#36091): This file can be refactored to look like lsp_test.go
-// when marker support gets added for go.mod files.
func TestModfileRemainsUnchanged(t *testing.T) {
ctx := tests.Context(t)
- cache := cache.New(nil)
+ cache := cache.New(nil, nil)
session := cache.NewSession()
options := tests.DefaultOptions()
options.TempModfile = true
@@ -46,11 +41,11 @@
if err != nil {
t.Fatal(err)
}
- _, snapshot, err := session.NewView(ctx, "diagnostics_test", span.FileURI(folder), options)
+ _, snapshot, err := session.NewView(ctx, "diagnostics_test", span.URIFromPath(folder), options)
if err != nil {
t.Fatal(err)
}
- if !hasTempModfile(ctx, snapshot) {
+ if _, t := snapshot.View().ModFiles(); t == "" {
return
}
after, err := ioutil.ReadFile(filepath.Join(folder, "go.mod"))
@@ -61,119 +56,3 @@
t.Errorf("the real go.mod file was changed even when tempModfile=true")
}
}
-
-// TODO(golang/go#36091): This file can be refactored to look like lsp_test.go
-// when marker support gets added for go.mod files.
-func TestDiagnostics(t *testing.T) {
- ctx := tests.Context(t)
- cache := cache.New(nil)
- session := cache.NewSession()
- options := tests.DefaultOptions()
- options.TempModfile = true
- options.Env = append(os.Environ(), "GOPACKAGESDRIVER=off", "GOROOT=")
-
- for _, tt := range []struct {
- testdir string
- want []source.Diagnostic
- }{
- {
- testdir: "indirect",
- want: []source.Diagnostic{
- {
- Message: "golang.org/x/tools should be a direct dependency.",
- Source: "go mod tidy",
- // TODO(golang/go#36091): When marker support gets added for go.mod files, we
- // can remove these hard coded positions.
- Range: protocol.Range{Start: getRawPos(4, 62), End: getRawPos(4, 73)},
- Severity: protocol.SeverityWarning,
- },
- },
- },
- {
- testdir: "unused",
- want: []source.Diagnostic{
- {
- Message: "golang.org/x/tools is not used in this module.",
- Source: "go mod tidy",
- Range: protocol.Range{Start: getRawPos(4, 0), End: getRawPos(4, 61)},
- Severity: protocol.SeverityWarning,
- },
- },
- },
- {
- testdir: "invalidrequire",
- want: []source.Diagnostic{
- {
- Message: "usage: require module/path v1.2.3",
- Source: "syntax",
- Range: protocol.Range{Start: getRawPos(4, 0), End: getRawPos(4, 16)},
- Severity: protocol.SeverityError,
- },
- },
- },
- {
- testdir: "invalidgo",
- want: []source.Diagnostic{
- {
- Message: "usage: go 1.23",
- Source: "syntax",
- Range: protocol.Range{Start: getRawPos(2, 0), End: getRawPos(2, 3)},
- Severity: protocol.SeverityError,
- },
- },
- },
- {
- testdir: "unknowndirective",
- want: []source.Diagnostic{
- {
- Message: "unknown directive: yo",
- Source: "syntax",
- Range: protocol.Range{Start: getRawPos(6, 0), End: getRawPos(6, 1)},
- Severity: protocol.SeverityError,
- },
- },
- },
- } {
- t.Run(tt.testdir, func(t *testing.T) {
- // TODO: Once we refactor this to work with go/packages/packagestest. We do not
- // need to copy to a temporary directory.
- folder, err := tests.CopyFolderToTempDir(filepath.Join("testdata", tt.testdir))
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(folder)
- _, snapshot, err := session.NewView(ctx, "diagnostics_test", span.FileURI(folder), options)
- if err != nil {
- t.Fatal(err)
- }
- // TODO: Add testing for when the -modfile flag is turned off and we still get diagnostics.
- if !hasTempModfile(ctx, snapshot) {
- return
- }
- reports, err := Diagnostics(ctx, snapshot)
- if err != nil {
- t.Fatal(err)
- }
- if len(reports) != 1 {
- t.Errorf("expected 1 diagnostic, got %d", len(reports))
- }
- for fh, got := range reports {
- if diff := tests.DiffDiagnostics(fh.URI, tt.want, got); diff != "" {
- t.Error(diff)
- }
- }
- })
- }
-}
-
-func hasTempModfile(ctx context.Context, snapshot source.Snapshot) bool {
- _, t := snapshot.View().ModFiles()
- return t != ""
-}
-
-func getRawPos(line, character int) protocol.Position {
- return protocol.Position{
- Line: float64(line),
- Character: float64(character),
- }
-}
diff --git a/internal/lsp/mod/testdata/indirect/go.mod b/internal/lsp/mod/testdata/indirect/go.mod
deleted file mode 100644
index 2e5dc13..0000000
--- a/internal/lsp/mod/testdata/indirect/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module indirect
-
-go 1.12
-
-require golang.org/x/tools v0.0.0-20191219192050-56b0b28a00f7 // indirect
diff --git a/internal/lsp/mod/testdata/indirect/go.mod.golden b/internal/lsp/mod/testdata/indirect/go.mod.golden
deleted file mode 100644
index 7e4be77..0000000
--- a/internal/lsp/mod/testdata/indirect/go.mod.golden
+++ /dev/null
@@ -1,5 +0,0 @@
-module indirect
-
-go 1.12
-
-require golang.org/x/tools v0.0.0-20191219192050-56b0b28a00f7
diff --git a/internal/lsp/mod/testdata/indirect/go.sum b/internal/lsp/mod/testdata/indirect/go.sum
deleted file mode 100644
index 8fec86c..0000000
--- a/internal/lsp/mod/testdata/indirect/go.sum
+++ /dev/null
@@ -1,12 +0,0 @@
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/tools v0.0.0-20191219192050-56b0b28a00f7 h1:u+nComwpgIe2VK1OTg8C74VQWda+MuB+wkIEsqFeoxY=
-golang.org/x/tools v0.0.0-20191219192050-56b0b28a00f7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/lsp/mod/testdata/indirect/main.go b/internal/lsp/mod/testdata/indirect/main.go
deleted file mode 100644
index a596abf..0000000
--- a/internal/lsp/mod/testdata/indirect/main.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// Package indirect does something
-package indirect
-
-import (
- "golang.org/x/tools/go/packages"
-)
-
-func Yo() {
- var _ packages.Config
-}
diff --git a/internal/lsp/mod/testdata/invalidgo/go.mod b/internal/lsp/mod/testdata/invalidgo/go.mod
deleted file mode 100644
index ca06b60..0000000
--- a/internal/lsp/mod/testdata/invalidgo/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module invalidgo
-
-go 1
-
-require golang.org/x/tools v0.0.0-20191219192050-56b0b28a00f7 // indirect
diff --git a/internal/lsp/mod/testdata/invalidgo/main.go b/internal/lsp/mod/testdata/invalidgo/main.go
deleted file mode 100644
index 5577a36..0000000
--- a/internal/lsp/mod/testdata/invalidgo/main.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// Package invalidgo does something
-package invalidgo
-
-import (
- "golang.org/x/tools/go/packages"
-)
-
-func Yo() {
- var _ packages.Config
-}
diff --git a/internal/lsp/mod/testdata/invalidrequire/go.mod b/internal/lsp/mod/testdata/invalidrequire/go.mod
deleted file mode 100644
index 98c5b05..0000000
--- a/internal/lsp/mod/testdata/invalidrequire/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module invalidrequire
-
-go 1.12
-
-require golang.or
diff --git a/internal/lsp/mod/testdata/invalidrequire/main.go b/internal/lsp/mod/testdata/invalidrequire/main.go
deleted file mode 100644
index dd24341..0000000
--- a/internal/lsp/mod/testdata/invalidrequire/main.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// Package invalidrequire does something
-package invalidrequire
-
-import (
- "golang.org/x/tools/go/packages"
-)
-
-func Yo() {
- var _ packages.Config
-}
diff --git a/internal/lsp/mod/testdata/unchanged/go.mod b/internal/lsp/mod/testdata/unchanged/go.mod
index e5bdaef..e3d13ce 100644
--- a/internal/lsp/mod/testdata/unchanged/go.mod
+++ b/internal/lsp/mod/testdata/unchanged/go.mod
@@ -1,3 +1 @@
module unchanged
-
-go 1.14
diff --git a/internal/lsp/mod/testdata/unknowndirective/go.mod b/internal/lsp/mod/testdata/unknowndirective/go.mod
deleted file mode 100644
index 4f07729..0000000
--- a/internal/lsp/mod/testdata/unknowndirective/go.mod
+++ /dev/null
@@ -1,7 +0,0 @@
-module unknowndirective
-
-go 1.12
-
-require golang.org/x/tools v0.0.0-20191219192050-56b0b28a00f7
-
-yo
diff --git a/internal/lsp/mod/testdata/unknowndirective/main.go b/internal/lsp/mod/testdata/unknowndirective/main.go
deleted file mode 100644
index 5ee984e..0000000
--- a/internal/lsp/mod/testdata/unknowndirective/main.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// Package unknowndirective does something
-package unknowndirective
-
-import (
- "golang.org/x/tools/go/packages"
-)
-
-func Yo() {
- var _ packages.Config
-}
diff --git a/internal/lsp/mod/testdata/unused/go.mod b/internal/lsp/mod/testdata/unused/go.mod
deleted file mode 100644
index 2c2f19c..0000000
--- a/internal/lsp/mod/testdata/unused/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module unused
-
-go 1.12
-
-require golang.org/x/tools v0.0.0-20191219192050-56b0b28a00f7
diff --git a/internal/lsp/mod/testdata/unused/go.mod.golden b/internal/lsp/mod/testdata/unused/go.mod.golden
deleted file mode 100644
index 34ca63a..0000000
--- a/internal/lsp/mod/testdata/unused/go.mod.golden
+++ /dev/null
@@ -1,3 +0,0 @@
-module unused
-
-go 1.12
diff --git a/internal/lsp/mod/testdata/unused/go.sum b/internal/lsp/mod/testdata/unused/go.sum
deleted file mode 100644
index e8a4a48..0000000
--- a/internal/lsp/mod/testdata/unused/go.sum
+++ /dev/null
@@ -1,11 +0,0 @@
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/tools v0.0.0-20191219192050-56b0b28a00f7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/lsp/protocol/context.go b/internal/lsp/protocol/context.go
index d024e00..7e9246d 100644
--- a/internal/lsp/protocol/context.go
+++ b/internal/lsp/protocol/context.go
@@ -5,14 +5,9 @@
"fmt"
"golang.org/x/tools/internal/telemetry"
- "golang.org/x/tools/internal/telemetry/export"
"golang.org/x/tools/internal/xcontext"
)
-func init() {
- export.AddExporters(logExporter{})
-}
-
type contextKey int
const (
@@ -23,16 +18,7 @@
return context.WithValue(ctx, clientKey, client)
}
-// logExporter sends the log event back to the client if there is one stored on the
-// context.
-type logExporter struct{}
-
-func (logExporter) StartSpan(context.Context, *telemetry.Span) {}
-func (logExporter) FinishSpan(context.Context, *telemetry.Span) {}
-func (logExporter) Metric(context.Context, telemetry.MetricData) {}
-func (logExporter) Flush() {}
-
-func (logExporter) Log(ctx context.Context, event telemetry.Event) {
+func LogEvent(ctx context.Context, event telemetry.Event) {
client, ok := ctx.Value(clientKey).(Client)
if !ok {
return
diff --git a/internal/lsp/protocol/protocol.go b/internal/lsp/protocol/protocol.go
index 8b3777f..d66941a 100644
--- a/internal/lsp/protocol/protocol.go
+++ b/internal/lsp/protocol/protocol.go
@@ -20,19 +20,44 @@
RequestCancelledError = -32800
)
-type canceller struct{ jsonrpc2.EmptyHandler }
-
type clientHandler struct {
jsonrpc2.EmptyHandler
client Client
}
+// ClientHandler returns a jsonrpc2.Handler that handles the LSP client
+// protocol.
+func ClientHandler(client Client) jsonrpc2.Handler {
+ return &clientHandler{client: client}
+}
+
type serverHandler struct {
jsonrpc2.EmptyHandler
server Server
}
-func (canceller) Request(ctx context.Context, conn *jsonrpc2.Conn, direction jsonrpc2.Direction, r *jsonrpc2.WireRequest) context.Context {
+// ServerHandler returns a jsonrpc2.Handler that handles the LSP server
+// protocol.
+func ServerHandler(server Server) jsonrpc2.Handler {
+ return &serverHandler{server: server}
+}
+
+// ClientDispatcher returns a Client that dispatches LSP requests across the
+// given jsonrpc2 connection.
+func ClientDispatcher(conn *jsonrpc2.Conn) Client {
+ return &clientDispatcher{Conn: conn}
+}
+
+// ServerDispatcher returns a Server that dispatches LSP requests across the
+// given jsonrpc2 connection.
+func ServerDispatcher(conn *jsonrpc2.Conn) Server {
+ return &serverDispatcher{Conn: conn}
+}
+
+// Canceller is a jsonrpc2.Handler that handles LSP request cancellation.
+type Canceller struct{ jsonrpc2.EmptyHandler }
+
+func (Canceller) Request(ctx context.Context, conn *jsonrpc2.Conn, direction jsonrpc2.Direction, r *jsonrpc2.WireRequest) context.Context {
if direction == jsonrpc2.Receive && r.Method == "$/cancelRequest" {
var params CancelParams
if err := json.Unmarshal(*r.Params, ¶ms); err != nil {
@@ -53,39 +78,23 @@
return ctx
}
-func (canceller) Cancel(ctx context.Context, conn *jsonrpc2.Conn, id jsonrpc2.ID, cancelled bool) bool {
+func (Canceller) Cancel(ctx context.Context, conn *jsonrpc2.Conn, id jsonrpc2.ID, cancelled bool) bool {
if cancelled {
return false
}
ctx = xcontext.Detach(ctx)
ctx, done := trace.StartSpan(ctx, "protocol.canceller")
defer done()
- conn.Notify(ctx, "$/cancelRequest", &CancelParams{ID: id})
+ // Note that only *jsonrpc2.ID implements json.Marshaler.
+ conn.Notify(ctx, "$/cancelRequest", &CancelParams{ID: &id})
return true
}
-func (canceller) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
+func (Canceller) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
// Hide cancellations from downstream handlers.
return r.Method == "$/cancelRequest"
}
-func NewClient(ctx context.Context, stream jsonrpc2.Stream, client Client) (context.Context, *jsonrpc2.Conn, Server) {
- ctx = WithClient(ctx, client)
- conn := jsonrpc2.NewConn(stream)
- conn.AddHandler(&clientHandler{client: client})
- conn.AddHandler(&canceller{})
- return ctx, conn, &serverDispatcher{Conn: conn}
-}
-
-func NewServer(ctx context.Context, stream jsonrpc2.Stream, server Server) (context.Context, *jsonrpc2.Conn, Client) {
- conn := jsonrpc2.NewConn(stream)
- client := &clientDispatcher{Conn: conn}
- ctx = WithClient(ctx, client)
- conn.AddHandler(&serverHandler{server: server})
- conn.AddHandler(&canceller{})
- return ctx, conn, client
-}
-
func sendParseError(ctx context.Context, req *jsonrpc2.Request, err error) {
if _, ok := err.(*jsonrpc2.Error); !ok {
err = jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err)
diff --git a/internal/lsp/protocol/span.go b/internal/lsp/protocol/span.go
index 5c9c4d1..8363d5c 100644
--- a/internal/lsp/protocol/span.go
+++ b/internal/lsp/protocol/span.go
@@ -19,8 +19,16 @@
Content []byte
}
-func NewURI(uri span.URI) string {
- return string(uri)
+func URIFromSpanURI(uri span.URI) DocumentURI {
+ return DocumentURI(uri)
+}
+
+func URIFromPath(path string) DocumentURI {
+ return URIFromSpanURI(span.URIFromPath(path))
+}
+
+func (u DocumentURI) SpanURI() span.URI {
+ return span.URIFromURI(string(u))
}
func (m *ColumnMapper) Location(s span.Span) (Location, error) {
@@ -28,7 +36,7 @@
if err != nil {
return Location{}, err
}
- return Location{URI: NewURI(s.URI()), Range: rng}, nil
+ return Location{URI: URIFromSpanURI(s.URI()), Range: rng}, nil
}
func (m *ColumnMapper) Range(s span.Span) (Range, error) {
diff --git a/internal/lsp/protocol/tsclient.go b/internal/lsp/protocol/tsclient.go
index ab2abc9..53327ed 100644
--- a/internal/lsp/protocol/tsclient.go
+++ b/internal/lsp/protocol/tsclient.go
@@ -3,7 +3,7 @@
// Package protocol contains data types and code for LSP jsonrpcs
// generated automatically from vscode-languageserver-node
// commit: 7b90c29d0cb5cd7b9c41084f6cb3781a955adeba
-// last fetched Thu Jan 23 2020 11:10:31 GMT-0500 (Eastern Standard Time)
+// last fetched Wed Feb 12 2020 17:16:47 GMT-0500 (Eastern Standard Time)
// Code generated (see typescript/README.md) DO NOT EDIT.
diff --git a/internal/lsp/protocol/tsprotocol.go b/internal/lsp/protocol/tsprotocol.go
index e8be364..f5b66fe 100644
--- a/internal/lsp/protocol/tsprotocol.go
+++ b/internal/lsp/protocol/tsprotocol.go
@@ -1,7 +1,7 @@
// Package protocol contains data types and code for LSP jsonrpcs
// generated automatically from vscode-languageserver-node
// commit: 7b90c29d0cb5cd7b9c41084f6cb3781a955adeba
-// last fetched Thu Jan 23 2020 11:10:31 GMT-0500 (Eastern Standard Time)
+// last fetched Wed Feb 12 2020 17:16:47 GMT-0500 (Eastern Standard Time)
package protocol
// Code generated (see typescript/README.md) DO NOT EDIT.
@@ -1532,7 +1532,7 @@
/**
* A tagging type for string properties that are actually URIs.
*/
-type DocumentURI = string
+type DocumentURI string
/**
* The client capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest).
diff --git a/internal/lsp/protocol/tsserver.go b/internal/lsp/protocol/tsserver.go
index 24a9ad1..7853449 100644
--- a/internal/lsp/protocol/tsserver.go
+++ b/internal/lsp/protocol/tsserver.go
@@ -3,7 +3,7 @@
// Package protocol contains data types and code for LSP jsonrpcs
// generated automatically from vscode-languageserver-node
// commit: 7b90c29d0cb5cd7b9c41084f6cb3781a955adeba
-// last fetched Thu Jan 23 2020 11:10:31 GMT-0500 (Eastern Standard Time)
+// last fetched Wed Feb 12 2020 17:16:47 GMT-0500 (Eastern Standard Time)
// Code generated (see typescript/README.md) DO NOT EDIT.
@@ -60,7 +60,7 @@
RangeFormatting(context.Context, *DocumentRangeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error)
OnTypeFormatting(context.Context, *DocumentOnTypeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error)
Rename(context.Context, *RenameParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error)
- PrepareRename(context.Context, *PrepareRenameParams) (interface{} /* Range | struct{; Range Range`json:"range"`; Placeholder string`json:"placeholder"`; } | nil*/, error)
+ PrepareRename(context.Context, *PrepareRenameParams) (*Range /*Range | { range: Range, placeholder: string } | null*/, error)
ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{} /*any | null*/, error)
PrepareCallHierarchy(context.Context, *CallHierarchyPrepareParams) ([]CallHierarchyItem /*CallHierarchyItem[] | null*/, error)
IncomingCalls(context.Context, *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall /*CallHierarchyIncomingCall[] | null*/, error)
@@ -920,12 +920,12 @@
return &result, nil
}
-func (s *serverDispatcher) PrepareRename(ctx context.Context, params *PrepareRenameParams) (interface{} /* Range | struct{; Range Range`json:"range"`; Placeholder string`json:"placeholder"`; } | nil*/, error) {
- var result interface{} /* Range | struct{; Range Range`json:"range"`; Placeholder string`json:"placeholder"`; } | nil*/
+func (s *serverDispatcher) PrepareRename(ctx context.Context, params *PrepareRenameParams) (*Range /*Range | { range: Range, placeholder: string } | null*/, error) {
+ var result Range /*Range | { range: Range, placeholder: string } | null*/
if err := s.Conn.Call(ctx, "textDocument/prepareRename", params, &result); err != nil {
return nil, err
}
- return result, nil
+ return &result, nil
}
func (s *serverDispatcher) ExecuteCommand(ctx context.Context, params *ExecuteCommandParams) (interface{} /*any | null*/, error) {
diff --git a/internal/lsp/protocol/typescript/code.ts b/internal/lsp/protocol/typescript/code.ts
index c37f62b..fe35d80 100644
--- a/internal/lsp/protocol/typescript/code.ts
+++ b/internal/lsp/protocol/typescript/code.ts
@@ -19,15 +19,15 @@
import * as fs from 'fs';
import * as ts from 'typescript';
import * as u from './util';
-import { constName, getComments, goName, loc, strKind } from './util';
+import {constName, getComments, goName, loc, strKind} from './util';
var program: ts.Program;
function parse() {
// this won't complain if some fnames don't exist
program = ts.createProgram(
- u.fnames,
- { target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS });
+ u.fnames,
+ {target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS});
program.getTypeChecker(); // finish type checking and assignment
}
@@ -35,14 +35,16 @@
let req = new Map<string, ts.NewExpression>(); // requests
let not = new Map<string, ts.NewExpression>(); // notifications
let ptypes = new Map<string, [ts.TypeNode, ts.TypeNode]>(); // req, resp types
-let receives = new Map<string, 'server' | 'client'>(); // who receives it
+let receives = new Map<string, 'server'|'client'>(); // who receives it
let rpcTypes = new Set<string>(); // types seen in the rpcs
function findRPCs(node: ts.Node) {
if (!ts.isModuleDeclaration(node)) {
return
- } if (!ts.isIdentifier(node.name)) {
- throw new Error(`expected Identifier, got ${strKind(node.name)} at ${loc(node)}`)
+ }
+ if (!ts.isIdentifier(node.name)) {
+ throw new Error(
+ `expected Identifier, got ${strKind(node.name)} at ${loc(node)}`)
}
let reqnot = req
let v = node.name.getText()
@@ -50,7 +52,8 @@
else if (!v.endsWith('Request')) return;
if (!ts.isModuleBlock(node.body)) {
- throw new Error(`expected ModuleBody got ${strKind(node.body)} at ${loc(node)}`)
+ throw new Error(
+ `expected ModuleBody got ${strKind(node.body)} at ${loc(node)}`)
}
let x: ts.ModuleBlock = node.body
// The story is to expect const method = 'textDocument/implementation'
@@ -59,48 +62,53 @@
let rpc: string = '';
let newNode: ts.NewExpression;
for (let i = 0; i < x.statements.length; i++) {
- const uu = x.statements[i]
+ const uu = x.statements[i];
if (!ts.isVariableStatement(uu)) continue;
- const dl: ts.VariableDeclarationList = uu.declarationList
- if (dl.declarations.length != 1) throw new Error(`expected a single decl at ${loc(dl)}`)
- const decl: ts.VariableDeclaration = dl.declarations[0]
+ const dl: ts.VariableDeclarationList = uu.declarationList;
+ if (dl.declarations.length != 1)
+ throw new Error(`expected a single decl at ${loc(dl)}`);
+ const decl: ts.VariableDeclaration = dl.declarations[0];
const name = decl.name.getText()
// we want the initializers
- if (name == 'method') { // StringLiteral
- if (!ts.isStringLiteral(decl.initializer)) throw new Error(`expect StringLiteral at ${loc(decl)}`)
+ if (name == 'method') { // StringLiteral
+ if (!ts.isStringLiteral(decl.initializer))
+ throw new Error(`expect StringLiteral at ${loc(decl)}`);
rpc = decl.initializer.getText()
- } else if (name == 'type') { // NewExpression
- if (!ts.isNewExpression(decl.initializer)) throw new Error(`expecte new at ${loc(decl)}`)
+ }
+ else if (name == 'type') { // NewExpression
+ if (!ts.isNewExpression(decl.initializer))
+ throw new Error(`expecte new at ${loc(decl)}`);
const nn: ts.NewExpression = decl.initializer
newNode = nn
- const mtd = nn.arguments[0]
+ const mtd = nn.arguments[0];
if (ts.isStringLiteral(mtd)) rpc = mtd.getText();
switch (nn.typeArguments.length) {
- case 1: // exit
+ case 1: // exit
ptypes.set(rpc, [nn.typeArguments[0], null])
break;
- case 2: // notifications
+ case 2: // notifications
ptypes.set(rpc, [nn.typeArguments[0], null])
break;
- case 4:// request with no parameters
+ case 4: // request with no parameters
ptypes.set(rpc, [null, nn.typeArguments[0]])
break;
- case 5: // request req, resp, partial(?)
+ case 5: // request req, resp, partial(?)
ptypes.set(rpc, [nn.typeArguments[0], nn.typeArguments[1]])
break;
- default: throw new Error(`${nn.typeArguments.length} at ${loc(nn)}`)
+ default:
+ throw new Error(`${nn.typeArguments.length} at ${loc(nn)}`)
}
}
}
- if (rpc == '') throw new Error(`no name found at ${loc(x)}`)
+ if (rpc == '') throw new Error(`no name found at ${loc(x)}`);
// remember the implied types
const [a, b] = ptypes.get(rpc);
- const add = function (n: ts.Node) {
+ const add = function(n: ts.Node) {
rpcTypes.add(goName(n.getText()))
};
underlying(a, add);
underlying(b, add);
- rpc = rpc.substring(1, rpc.length - 1) // 'exit'
+ rpc = rpc.substring(1, rpc.length - 1); // 'exit'
reqnot.set(rpc, newNode)
}
@@ -122,8 +130,8 @@
// it would be nice to have some independent check on this
// (this logic fails if the server ever sends $/canceRequest
// or $/progress)
- req.forEach((_, k) => { receives.set(k, 'server') });
- not.forEach((_, k) => { receives.set(k, 'server') });
+ req.forEach((_, k) => {receives.set(k, 'server')});
+ not.forEach((_, k) => {receives.set(k, 'server')});
receives.set('window/showMessage', 'client');
receives.set('window/showMessageRequest', 'client');
receives.set('window/logMessage', 'client');
@@ -158,22 +166,22 @@
function newData(n: ts.Node, nm: string): Data {
return {
me: n, name: goName(nm),
- generics: ts.createNodeArray<ts.TypeParameterDeclaration>(), as: ts.createNodeArray<ts.HeritageClause>(),
- properties: ts.createNodeArray<ts.TypeElement>(), alias: undefined,
- statements: ts.createNodeArray<ts.Statement>(),
- enums: ts.createNodeArray<ts.EnumMember>(),
- members: ts.createNodeArray<ts.PropertyDeclaration>(),
+ generics: ts.createNodeArray<ts.TypeParameterDeclaration>(), as: ts.createNodeArray<ts.HeritageClause>(),
+ properties: ts.createNodeArray<ts.TypeElement>(), alias: undefined,
+ statements: ts.createNodeArray<ts.Statement>(),
+ enums: ts.createNodeArray<ts.EnumMember>(),
+ members: ts.createNodeArray<ts.PropertyDeclaration>(),
}
}
// for debugging, produce a skeleton description
function strData(d: Data): string {
- const f = function (na: ts.NodeArray<any>): number {
+ const f = function(na: ts.NodeArray<any>): number {
return na.length
};
return `D(${d.name}) g;${f(d.generics)} a:${f(d.as)} p:${f(d.properties)} s:${
- f(d.statements)} e:${f(d.enums)} m:${f(d.members)} ${
- d.alias != undefined}`
+ f(d.statements)} e:${f(d.enums)} m:${f(d.members)} ${
+ d.alias != undefined}`
}
let data = new Map<string, Data>(); // parsed data types
@@ -184,16 +192,16 @@
function genTypes(node: ts.Node) {
// Ignore top-level items that can't produce output
if (ts.isExpressionStatement(node) || ts.isFunctionDeclaration(node) ||
- ts.isImportDeclaration(node) || ts.isVariableStatement(node) ||
- ts.isExportDeclaration(node) || ts.isEmptyStatement(node) ||
- node.kind == ts.SyntaxKind.EndOfFileToken) {
+ ts.isImportDeclaration(node) || ts.isVariableStatement(node) ||
+ ts.isExportDeclaration(node) || ts.isEmptyStatement(node) ||
+ node.kind == ts.SyntaxKind.EndOfFileToken) {
return;
}
if (ts.isInterfaceDeclaration(node)) {
const v: ts.InterfaceDeclaration = node;
// need to check the members, many of which are disruptive
let mems: ts.TypeElement[] = [];
- const f = function (t: ts.TypeElement) {
+ const f = function(t: ts.TypeElement) {
if (ts.isPropertySignature(t)) {
mems.push(t);
} else if (ts.isMethodSignature(t) || ts.isCallSignatureDeclaration(t)) {
@@ -208,7 +216,7 @@
};
v.members.forEach(f);
if (mems.length == 0 && !v.heritageClauses &&
- v.name.getText() != 'InitializedParams') {
+ v.name.getText() != 'InitializedParams') {
return // really? (Don't seem to need any of these)
};
// Found one we want
@@ -232,7 +240,7 @@
// (at the top level)
// Unfortunately this is false for TraceValues
if (ts.isUnionTypeNode(v.type) &&
- v.type.types.every((n: ts.TypeNode) => ts.isLiteralTypeNode(n))) {
+ v.type.types.every((n: ts.TypeNode) => ts.isLiteralTypeNode(n))) {
if (x.name != 'TraceValues') return;
}
if (v.typeParameters) {
@@ -251,7 +259,7 @@
const b: ts.ModuleBlock = v.body;
var s: ts.Statement[] = [];
// we don't want most of these
- const fx = function (x: ts.Statement) {
+ const fx = function(x: ts.Statement) {
if (ts.isFunctionDeclaration(x)) {
return
};
@@ -259,7 +267,8 @@
return
};
if (!ts.isVariableStatement(x))
- throw new Error(`expected VariableStatment ${loc(x)} ${strKind(x)} ${x.getText()}`);
+ throw new Error(
+ `expected VariableStatment ${loc(x)} ${strKind(x)} ${x.getText()}`);
if (hasNewExpression(x)) {
return
};
@@ -285,7 +294,7 @@
const v: ts.ClassDeclaration = node;
var d: ts.PropertyDeclaration[] = [];
// look harder at the PropertyDeclarations.
- const wanted = function (c: ts.ClassElement): string {
+ const wanted = function(c: ts.ClassElement): string {
if (ts.isConstructorDeclaration(c)) {
return ''
};
@@ -320,7 +329,7 @@
c.as = v.heritageClauses
}
if (data.has(c.name))
- throw new Error(`Class dup ${loc(c.me)} and ${loc(data.get(c.name).me)}`)
+ throw new Error(`Class dup ${loc(c.me)} and ${loc(data.get(c.name).me)}`);
data.set(c.name, c);
} else {
throw new Error(`unexpected ${strKind(node)} ${loc(node)} `)
@@ -336,7 +345,8 @@
}
const ax = `(${a.statements.length},${a.properties.length})`
const bx = `(${b.statements.length},${b.properties.length})`
- //console.log(`397 ${a.name}${ax}${bx}\n${a.me.getText()}\n${b.me.getText()}\n`)
+ // console.log(`397
+ // ${a.name}${ax}${bx}\n${a.me.getText()}\n${b.me.getText()}\n`)
switch (a.name) {
case 'InitializeError':
case 'MessageType':
@@ -348,11 +358,11 @@
case 'CancellationToken':
// want the Interface
return a.properties.length > 0 ? a : b;
- case 'TextDocumentContentChangeEvent': // almost the same
+ case 'TextDocumentContentChangeEvent': // almost the same
return a;
}
console.log(
- `${strKind(a.me)} ${strKind(b.me)} ${a.name} ${loc(a.me)} ${loc(b.me)}`)
+ `${strKind(a.me)} ${strKind(b.me)} ${a.name} ${loc(a.me)} ${loc(b.me)}`)
throw new Error(`Fix dataMerge for ${a.name}`)
}
@@ -375,19 +385,19 @@
// helper function to find underlying types
function underlying(n: ts.Node, f: (n: ts.Node) => void) {
if (!n) return;
- const ff = function (n: ts.Node) {
+ const ff = function(n: ts.Node) {
underlying(n, f)
};
if (ts.isIdentifier(n)) {
f(n)
} else if (
- n.kind == ts.SyntaxKind.StringKeyword ||
- n.kind == ts.SyntaxKind.NumberKeyword ||
- n.kind == ts.SyntaxKind.AnyKeyword ||
- n.kind == ts.SyntaxKind.NullKeyword ||
- n.kind == ts.SyntaxKind.BooleanKeyword ||
- n.kind == ts.SyntaxKind.ObjectKeyword ||
- n.kind == ts.SyntaxKind.VoidKeyword) {
+ n.kind == ts.SyntaxKind.StringKeyword ||
+ n.kind == ts.SyntaxKind.NumberKeyword ||
+ n.kind == ts.SyntaxKind.AnyKeyword ||
+ n.kind == ts.SyntaxKind.NullKeyword ||
+ n.kind == ts.SyntaxKind.BooleanKeyword ||
+ n.kind == ts.SyntaxKind.ObjectKeyword ||
+ n.kind == ts.SyntaxKind.VoidKeyword) {
// nothing to do
} else if (ts.isTypeReferenceNode(n)) {
f(n.typeName)
@@ -408,8 +418,8 @@
} else if (ts.isParenthesizedTypeNode(n)) {
underlying(n.type, f)
} else if (
- ts.isLiteralTypeNode(n) || ts.isVariableStatement(n) ||
- ts.isTupleTypeNode(n)) {
+ ts.isLiteralTypeNode(n) || ts.isVariableStatement(n) ||
+ ts.isTupleTypeNode(n)) {
// we only see these in moreTypes, but they are handled elsewhere
return;
} else if (ts.isEnumMember(n)) {
@@ -424,8 +434,8 @@
// Simplest way to the transitive closure is to stabilize the size of seenTypes
// but it is slow
function moreTypes() {
- const extra = function (s: string) {
- if (!data.has(s)) throw new Error(`moreTypes needs ${s}`)
+ const extra = function(s: string) {
+ if (!data.has(s)) throw new Error(`moreTypes needs ${s}`);
seenTypes.set(s, data.get(s))
};
rpcTypes.forEach(extra); // all the types needed by the rpcs
@@ -441,17 +451,17 @@
old = seenTypes.size
const m = new Map<string, Data>();
- const add = function (n: ts.Node) {
+ const add = function(n: ts.Node) {
const nm = goName(n.getText());
if (seenTypes.has(nm) || m.has(nm)) return;
// For generic parameters, this might set it to undefined
m.set(nm, data.get(nm));
};
// expect all the heritage clauses have single Identifiers
- const h = function (n: ts.Node) {
+ const h = function(n: ts.Node) {
underlying(n, add);
};
- const f = function (x: ts.NodeArray<ts.Node>) {
+ const f = function(x: ts.NodeArray<ts.Node>) {
x.forEach(h)
};
seenTypes.forEach((d: Data) => d && f(d.as))
@@ -480,11 +490,11 @@
} else if (d.enums.length > 0) {
goEnum(d, nm);
} else if (
- d.properties.length > 0 || d.as.length > 0 || nm == 'InitializedParams') {
+ d.properties.length > 0 || d.as.length > 0 || nm == 'InitializedParams') {
goInterface(d, nm);
} else
throw new Error(
- `more cases in toGo ${nm} ${d.as.length} ${d.generics.length} `)
+ `more cases in toGo ${nm} ${d.as.length} ${d.generics.length} `)
}
// these fields need a *
@@ -499,7 +509,7 @@
let ans = `type ${goName(nm)} struct {\n`;
// generate the code for each member
- const g = function (n: ts.TypeElement) {
+ const g = function(n: ts.TypeElement) {
if (!ts.isPropertySignature(n))
throw new Error(`expected PropertySignature got ${strKind(n)} `);
ans = ans.concat(getComments(n));
@@ -516,7 +526,7 @@
d.properties.forEach(g)
// heritage clauses become embedded types
// check they are all Identifiers
- const f = function (n: ts.ExpressionWithTypeArguments) {
+ const f = function(n: ts.ExpressionWithTypeArguments) {
if (!ts.isIdentifier(n.expression))
throw new Error(`Interface ${nm} heritage ${strKind(n.expression)} `);
ans = ans.concat(goName(n.expression.getText()), '\n')
@@ -539,7 +549,7 @@
// They are VariableStatements with x.declarationList having a single
// VariableDeclaration
let isNumeric = false;
- const f = function (n: ts.Statement, i: number) {
+ const f = function(n: ts.Statement, i: number) {
if (!ts.isVariableStatement(n)) {
throw new Error(` ${nm} ${i} expected VariableStatement,
got ${strKind(n)}`);
@@ -566,7 +576,7 @@
// generate Go code for an enum. Both types and named constants
function goEnum(d: Data, nm: string) {
let isNumeric = false
- const f = function (v: ts.EnumMember, j: number) { // same as goModule
+ const f = function(v: ts.EnumMember, j: number) { // same as goModule
if (!v.initializer)
throw new Error(`goEnum no initializer ${nm} ${j} ${v.name.getText()}`);
isNumeric = strKind(v.initializer) == 'NumericLiteral';
@@ -587,11 +597,12 @@
if (d.as.length != 0 || d.generics.length != 0) {
if (nm != 'ServerCapabilities')
throw new Error(`${nm} has extra fields(${d.as.length},${
- d.generics.length}) ${d.me.getText()}`);
+ d.generics.length}) ${d.me.getText()}`);
}
typesOut.push(getComments(d.me))
// d.alias doesn't seem to have comments
- typesOut.push(`type ${goName(nm)} = ${goType(d.alias, nm)}\n`)
+ let aliasStr = goName(nm) == "DocumentURI" ? " " : " = "
+ typesOut.push(`type ${goName(nm)}${aliasStr}${goType(d.alias, nm)}\n`)
}
// return a go type and maybe an assocated javascript tag
@@ -628,7 +639,7 @@
const v = goTypeLiteral(n, nm);
return v
} else if (ts.isTupleTypeNode(n)) {
- if (n.getText() == '[number, number]') return '[]float64'
+ if (n.getText() == '[number, number]') return '[]float64';
throw new Error(`goType unexpected Tuple ${n.getText()}`)
}
throw new Error(`${strKind(n)} goType unexpected ${n.getText()} for ${nm}`)
@@ -661,16 +672,21 @@
if (nm == 'renameProvider') return `interface{} ${help}`;
return `${goType(n.types[0], 'b')} ${help}`
}
- if (b == 'ArrayType') return `${goType(n.types[1], 'c')} ${help}`
- if (a == 'TypeReference' && a == b) return `interface{} ${help}`
+ if (b == 'ArrayType') return `${goType(n.types[1], 'c')} ${help}`;
+ if (a == 'TypeReference' && a == b) return `interface{} ${help}`;
if (a == 'StringKeyword') return `string ${help}`;
if (a == 'TypeLiteral' && nm == 'TextDocumentContentChangeEvent') {
return `${goType(n.types[0], nm)}`
}
- throw new Error(`724 ${a} ${b} ${n.getText()} ${loc(n)}`)
- case 3: const aa = strKind(n.types[0])
+ throw new Error(`724 ${a} ${b} ${n.getText()} ${loc(n)}`);
+ case 3:
+ const aa = strKind(n.types[0])
const bb = strKind(n.types[1])
const cc = strKind(n.types[2])
+ if (nm == 'textDocument/prepareRename') {
+ // want Range, not interface{}
+ return `${goType(n.types[0], nm)} ${help}`
+ }
if (nm == 'DocumentFilter') {
// not really a union. the first is enough, up to a missing
// omitempty but avoid repetitious comments
@@ -690,7 +706,7 @@
// check this is nm == 'textDocument/completion'
return `${goType(n.types[1], 'f')} ${help}`
}
- if (aa == 'LiteralType' && bb == aa && cc == aa) return `string ${help}`
+ if (aa == 'LiteralType' && bb == aa && cc == aa) return `string ${help}`;
break;
case 4:
if (nm == 'documentChanges') return `TextDocumentEdit ${help} `;
@@ -731,7 +747,7 @@
if (nm == 'ServerCapabilities') return expandIntersection(n);
let inner = '';
n.types.forEach(
- (t: ts.TypeNode) => { inner = inner.concat(goType(t, nm), '\n') });
+ (t: ts.TypeNode) => {inner = inner.concat(goType(t, nm), '\n')});
return `struct{ \n${inner}} `
}
@@ -740,7 +756,7 @@
// of them by name. The names that occur once can be output. The names
// that occur more than once need to be combined.
function expandIntersection(n: ts.IntersectionTypeNode): string {
- const bad = function (n: ts.Node, s: string) {
+ const bad = function(n: ts.Node, s: string) {
return new Error(`expandIntersection ${strKind(n)} ${s}`)
};
let props = new Map<string, ts.PropertySignature[]>();
@@ -774,10 +790,11 @@
if (!ts.isPropertySignature(b)) throw bad(b, 'D');
ans = ans.concat(getComments(b));
ans = ans.concat(
- goName(b.name.getText()), ' ', goType(b.type, 'a'), u.JSON(b), '\n')
+ goName(b.name.getText()), ' ', goType(b.type, 'a'), u.JSON(b), '\n')
} else if (a.type.kind == ts.SyntaxKind.ObjectKeyword) {
ans = ans.concat(getComments(a))
- ans = ans.concat(goName(a.name.getText()), ' ', 'interface{}', u.JSON(a), '\n')
+ ans = ans.concat(
+ goName(a.name.getText()), ' ', 'interface{}', u.JSON(a), '\n')
} else {
throw bad(a.type, `E ${a.getText()} in ${goName(k)} at ${loc(a)}`)
}
@@ -790,8 +807,8 @@
function goTypeLiteral(n: ts.TypeLiteralNode, nm: string): string {
let ans: string[] = []; // in case we generate a new extra type
- let res = 'struct{\n' // the actual answer usually
- const g = function (nx: ts.TypeElement) {
+ let res = 'struct{\n' // the actual answer usually
+ const g = function(nx: ts.TypeElement) {
// add the json, as in goInterface(). Strange inside union types.
if (ts.isPropertySignature(nx)) {
let json = u.JSON(nx);
@@ -941,7 +958,7 @@
let case1 = notNil;
if (a != '') {
if (extraTypes.has('Param' + nm)) a = 'Param' + nm
- case1 = `var params ${a}
+ case1 = `var params ${a}
if err := json.Unmarshal(*r.Params, ¶ms); err != nil {
sendParseError(ctx, r, err)
return true
@@ -975,7 +992,7 @@
if (indirect(b)) theRet = '&result';
callBody = `var result ${b}
if err := s.Conn.Call(ctx, "${m}", ${
- p2}, &result); err != nil {
+ p2}, &result); err != nil {
return nil, err
}
return ${theRet}, nil
@@ -1003,7 +1020,7 @@
if (seenNames.has(x)) {
// Resolve, ResolveCodeLens, ResolveDocumentLink
if (!x.startsWith('Resolve')) throw new Error(`expected Resolve, not ${x}`)
- x += m[0].toUpperCase() + m.substring(1, i)
+ x += m[0].toUpperCase() + m.substring(1, i)
}
seenNames.add(x);
return x;
@@ -1014,7 +1031,7 @@
if (s == '' || s == 'void') return false;
const skip = (x: string) => s.startsWith(x);
if (skip('[]') || skip('interface') || skip('Declaration') ||
- skip('Definition') || skip('DocumentSelector'))
+ skip('Definition') || skip('DocumentSelector'))
return false;
return true
}
@@ -1054,7 +1071,7 @@
side.outputFile = `ts${side.name}.go`;
side.fd = fs.openSync(side.outputFile, 'w');
}
- const f = function (s: string) {
+ const f = function(s: string) {
fs.writeSync(side.fd, s);
fs.writeSync(side.fd, '\n');
};
@@ -1071,10 +1088,10 @@
`);
const a = side.name[0].toUpperCase() + side.name.substring(1)
f(`type ${a} interface {`);
- side.methods.forEach((v) => { f(v) });
+ side.methods.forEach((v) => {f(v)});
f('}\n');
f(`func (h ${
- side.name}Handler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
+ side.name}Handler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
if delivered {
return false
}
@@ -1084,7 +1101,7 @@
return true
}
switch r.Method {`);
- side.cases.forEach((v) => { f(v) });
+ side.cases.forEach((v) => {f(v)});
f(`
}
}`);
@@ -1093,13 +1110,15 @@
*jsonrpc2.Conn
}
`);
- side.calls.forEach((v) => { f(v) });
+ side.calls.forEach((v) => {f(v)});
}
// Handling of non-standard requests, so we can add gopls-specific calls.
function nonstandardRequests() {
- server.methods.push('NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error)')
- server.calls.push(`func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) {
+ server.methods.push(
+ 'NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error)')
+ server.calls.push(
+ `func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) {
var result interface{}
if err := s.Conn.Call(ctx, method, params, &result); err != nil {
return nil, err
@@ -1127,7 +1146,7 @@
function main() {
if (u.gitHash != u.git()) {
throw new Error(
- `git hash mismatch, wanted\n${u.gitHash} but source is at\n${u.git()}`);
+ `git hash mismatch, wanted\n${u.gitHash} but source is at\n${u.git()}`);
}
u.createOutputFiles()
parse()
@@ -1154,13 +1173,11 @@
// 2. func (h *serverHandler) Deliver(...) { switch r.method }
// 3. func (x *xDispatcher) Method(ctx, parm)
not.forEach( // notifications
- (v, k) => {
- receives.get(k) == 'client' ? goNot(client, k) : goNot(server, k)
- });
+ (v, k) => {
+ receives.get(k) == 'client' ? goNot(client, k) : goNot(server, k)});
req.forEach( // requests
- (v, k) => {
- receives.get(k) == 'client' ? goReq(client, k) : goReq(server, k)
- });
+ (v, k) => {
+ receives.get(k) == 'client' ? goReq(client, k) : goReq(server, k)});
nonstandardRequests();
// find all the types implied by seenTypes and rpcs to try to avoid
// generating types that aren't used
diff --git a/internal/lsp/references.go b/internal/lsp/references.go
index d91fc16..57e92c0 100644
--- a/internal/lsp/references.go
+++ b/internal/lsp/references.go
@@ -9,26 +9,14 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
)
func (s *Server) references(ctx context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
- // Find all references to the identifier at the position.
- if fh.Identity().Kind != source.Go {
- return nil, nil
- }
-
- references, err := source.References(ctx, view.Snapshot(), fh, params.Position, params.Context.IncludeDeclaration)
+ references, err := source.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration)
if err != nil {
return nil, err
}
@@ -41,7 +29,7 @@
}
locations = append(locations, protocol.Location{
- URI: protocol.NewURI(ref.URI()),
+ URI: protocol.URIFromSpanURI(ref.URI()),
Range: refRange,
})
}
diff --git a/internal/lsp/regtest/definition_test.go b/internal/lsp/regtest/definition_test.go
new file mode 100644
index 0000000..502e7b7
--- /dev/null
+++ b/internal/lsp/regtest/definition_test.go
@@ -0,0 +1,85 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package regtest
+
+import (
+ "context"
+ "path"
+ "testing"
+
+ "golang.org/x/tools/internal/lsp/fake"
+)
+
+const internalDefinition = `
+-- go.mod --
+module mod
+
+go 1.12
+-- main.go --
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println(message)
+}
+-- const.go --
+package main
+
+const message = "Hello World."
+`
+
+func TestGoToInternalDefinition(t *testing.T) {
+ t.Parallel()
+ runner.Run(t, internalDefinition, func(ctx context.Context, t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ name, pos := env.GoToDefinition("main.go", fake.Pos{Line: 5, Column: 13})
+ if want := "const.go"; name != want {
+ t.Errorf("GoToDefinition: got file %q, want %q", name, want)
+ }
+ if want := (fake.Pos{Line: 2, Column: 6}); pos != want {
+ t.Errorf("GoToDefinition: got position %v, want %v", pos, want)
+ }
+ })
+}
+
+const stdlibDefinition = `
+-- go.mod --
+module mod
+
+go 1.12
+-- main.go --
+package main
+
+import (
+ "fmt"
+ "time"
+)
+
+func main() {
+ fmt.Println(time.Now())
+}`
+
+func TestGoToStdlibDefinition(t *testing.T) {
+ t.Skip("skipping due to golang.org/issues/37318")
+ t.Parallel()
+ runner.Run(t, stdlibDefinition, func(ctx context.Context, t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ name, pos := env.GoToDefinition("main.go", fake.Pos{Line: 8, Column: 19})
+ if got, want := path.Base(name), "time.go"; got != want {
+ t.Errorf("GoToDefinition: got file %q, want %q", name, want)
+ }
+
+ // Test that we can jump to definition from outside our workspace.
+ // See golang.org/issues/37045.
+ newName, newPos := env.GoToDefinition(name, pos)
+ if newName != name {
+ t.Errorf("GoToDefinition is not idempotent: got %q, want %q", newName, name)
+ }
+ if newPos != pos {
+ t.Errorf("GoToDefinition is not idempotent: got %v, want %v", newPos, pos)
+ }
+ })
+}
diff --git a/internal/lsp/regtest/diagnostics_test.go b/internal/lsp/regtest/diagnostics_test.go
new file mode 100644
index 0000000..1006b5e
--- /dev/null
+++ b/internal/lsp/regtest/diagnostics_test.go
@@ -0,0 +1,103 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package regtest
+
+import (
+ "context"
+ "testing"
+
+ "golang.org/x/tools/internal/lsp/fake"
+)
+
+const exampleProgram = `
+-- go.mod --
+module mod
+
+go 1.12
+-- main.go --
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello World.")
+}`
+
+func TestDiagnosticErrorInEditedFile(t *testing.T) {
+ t.Parallel()
+ runner.Run(t, exampleProgram, func(ctx context.Context, t *testing.T, env *Env) {
+ // Deleting the 'n' at the end of Println should generate a single error
+ // diagnostic.
+ edit := fake.NewEdit(5, 11, 5, 12, "")
+ env.OpenFile("main.go")
+ env.EditBuffer("main.go", edit)
+ env.Await(DiagnosticAt("main.go", 5, 5))
+ })
+}
+
+const brokenFile = `package main
+
+const Foo = "abc
+`
+
+func TestDiagnosticErrorInNewFile(t *testing.T) {
+ t.Parallel()
+ runner.Run(t, brokenFile, func(ctx context.Context, t *testing.T, env *Env) {
+ env.CreateBuffer("broken.go", brokenFile)
+ env.Await(DiagnosticAt("broken.go", 2, 12))
+ })
+}
+
+// badPackage contains a duplicate definition of the 'a' const.
+const badPackage = `
+-- go.mod --
+module mod
+
+go 1.12
+-- a.go --
+package consts
+
+const a = 1
+-- b.go --
+package consts
+
+const a = 2
+`
+
+func TestDiagnosticClearingOnEdit(t *testing.T) {
+ t.Parallel()
+ runner.Run(t, badPackage, func(ctx context.Context, t *testing.T, env *Env) {
+ env.OpenFile("b.go")
+ env.Await(DiagnosticAt("a.go", 2, 6), DiagnosticAt("b.go", 2, 6))
+
+ // Fix the error by editing the const name in b.go to `b`.
+ edit := fake.NewEdit(2, 6, 2, 7, "b")
+ env.EditBuffer("b.go", edit)
+ env.Await(EmptyDiagnostics("a.go"), EmptyDiagnostics("b.go"))
+ })
+}
+
+func TestDiagnosticClearingOnDelete(t *testing.T) {
+ t.Parallel()
+ runner.Run(t, badPackage, func(ctx context.Context, t *testing.T, env *Env) {
+ env.OpenFile("a.go")
+ env.Await(DiagnosticAt("a.go", 2, 6), DiagnosticAt("b.go", 2, 6))
+ env.RemoveFileFromWorkspace("b.go")
+
+ env.Await(EmptyDiagnostics("a.go"), EmptyDiagnostics("b.go"))
+ })
+}
+
+func TestDiagnosticClearingOnClose(t *testing.T) {
+ t.Parallel()
+ runner.Run(t, badPackage, func(ctx context.Context, t *testing.T, env *Env) {
+ env.CreateBuffer("c.go", `package consts
+
+const a = 3`)
+ env.Await(DiagnosticAt("a.go", 2, 6), DiagnosticAt("b.go", 2, 6), DiagnosticAt("c.go", 2, 6))
+ env.CloseBuffer("c.go")
+ env.Await(DiagnosticAt("a.go", 2, 6), DiagnosticAt("b.go", 2, 6), EmptyDiagnostics("c.go"))
+ })
+}
diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go
new file mode 100644
index 0000000..034deaa
--- /dev/null
+++ b/internal/lsp/regtest/env.go
@@ -0,0 +1,453 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package regtest provides an environment for writing regression tests.
+package regtest
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "golang.org/x/tools/internal/jsonrpc2/servertest"
+ "golang.org/x/tools/internal/lsp/cache"
+ "golang.org/x/tools/internal/lsp/debug"
+ "golang.org/x/tools/internal/lsp/fake"
+ "golang.org/x/tools/internal/lsp/lsprpc"
+ "golang.org/x/tools/internal/lsp/protocol"
+)
+
+// EnvMode is a bitmask that defines in which execution environments a test
+// should run.
+type EnvMode int
+
+const (
+ // Singleton mode uses a separate cache for each test.
+ Singleton EnvMode = 1 << iota
+ // Shared mode uses a Shared cache.
+ Shared
+ // Forwarded forwards connections to an in-process gopls instance.
+ Forwarded
+ // SeparateProcess runs a separate gopls process, and forwards connections to
+ // it.
+ SeparateProcess
+ // NormalModes runs tests in all modes.
+ NormalModes = Singleton | Shared | Forwarded
+)
+
+// A Runner runs tests in gopls execution environments, as specified by its
+// modes. For modes that share state (for example, a shared cache or common
+// remote), any tests that execute on the same Runner will share the same
+// state.
+type Runner struct {
+ defaultModes EnvMode
+ timeout time.Duration
+ goplsPath string
+
+ mu sync.Mutex
+ ts *servertest.TCPServer
+ socketDir string
+}
+
+// NewTestRunner creates a Runner with its shared state initialized, ready to
+// run tests.
+func NewTestRunner(modes EnvMode, testTimeout time.Duration, goplsPath string) *Runner {
+ return &Runner{
+ defaultModes: modes,
+ timeout: testTimeout,
+ goplsPath: goplsPath,
+ }
+}
+
+// Modes returns the bitmask of environment modes this runner is configured to
+// test.
+func (r *Runner) Modes() EnvMode {
+ return r.defaultModes
+}
+
+// getTestServer gets the test server instance to connect to, or creates one if
+// it doesn't exist.
+func (r *Runner) getTestServer() *servertest.TCPServer {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ if r.ts == nil {
+ di := debug.NewInstance("", "")
+ ss := lsprpc.NewStreamServer(cache.New(nil, di.State), false, di)
+ r.ts = servertest.NewTCPServer(context.Background(), ss)
+ }
+ return r.ts
+}
+
+// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running
+// tests. It's a trick to allow tests to find a binary to use to start a gopls
+// subprocess.
+const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS"
+
+func (r *Runner) getRemoteSocket(t *testing.T) string {
+ t.Helper()
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ const daemonFile = "gopls-test-daemon"
+ if r.socketDir != "" {
+ return filepath.Join(r.socketDir, daemonFile)
+ }
+
+ if r.goplsPath == "" {
+ t.Fatal("cannot run tests with a separate process unless a path to a gopls binary is configured")
+ }
+ var err error
+ r.socketDir, err = ioutil.TempDir("", "gopls-regtests")
+ if err != nil {
+ t.Fatalf("creating tempdir: %v", err)
+ }
+ socket := filepath.Join(r.socketDir, daemonFile)
+ args := []string{"serve", "-listen", "unix;" + socket, "-listen.timeout", "10s"}
+ cmd := exec.Command(r.goplsPath, args...)
+ cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true")
+ var stderr bytes.Buffer
+ cmd.Stderr = &stderr
+ go func() {
+ if err := cmd.Run(); err != nil {
+ panic(fmt.Sprintf("error running external gopls: %v\nstderr:\n%s", err, stderr.String()))
+ }
+ }()
+ return socket
+}
+
+// Close cleans up resource that have been allocated to this workspace.
+func (r *Runner) Close() error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ if r.ts != nil {
+ r.ts.Close()
+ }
+ if r.socketDir != "" {
+ os.RemoveAll(r.socketDir)
+ }
+ return nil
+}
+
+// Run executes the test function in the default configured gopls execution
+// modes. For each a test run, a new workspace is created containing the
+// un-txtared files specified by filedata.
+func (r *Runner) Run(t *testing.T, filedata string, test func(context.Context, *testing.T, *Env)) {
+ t.Helper()
+ r.RunInMode(r.defaultModes, t, filedata, test)
+}
+
+// RunInMode runs the test in the execution modes specified by the modes bitmask.
+func (r *Runner) RunInMode(modes EnvMode, t *testing.T, filedata string, test func(ctx context.Context, t *testing.T, e *Env)) {
+ t.Helper()
+ tests := []struct {
+ name string
+ mode EnvMode
+ getConnector func(context.Context, *testing.T) (servertest.Connector, func())
+ }{
+ {"singleton", Singleton, r.singletonEnv},
+ {"shared", Shared, r.sharedEnv},
+ {"forwarded", Forwarded, r.forwardedEnv},
+ {"separate_process", SeparateProcess, r.separateProcessEnv},
+ }
+
+ for _, tc := range tests {
+ tc := tc
+ if modes&tc.mode == 0 {
+ continue
+ }
+ t.Run(tc.name, func(t *testing.T) {
+ t.Helper()
+ ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
+ defer cancel()
+ ws, err := fake.NewWorkspace("lsprpc", []byte(filedata))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ws.Close()
+ ts, cleanup := tc.getConnector(ctx, t)
+ defer cleanup()
+ env := NewEnv(ctx, t, ws, ts)
+ defer func() {
+ if err := env.E.Shutdown(ctx); err != nil {
+ panic(err)
+ }
+ }()
+ test(ctx, t, env)
+ })
+ }
+}
+
+func (r *Runner) singletonEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
+ di := debug.NewInstance("", "")
+ ss := lsprpc.NewStreamServer(cache.New(nil, di.State), false, di)
+ ts := servertest.NewPipeServer(ctx, ss)
+ cleanup := func() {
+ ts.Close()
+ }
+ return ts, cleanup
+}
+
+func (r *Runner) sharedEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
+ return r.getTestServer(), func() {}
+}
+
+func (r *Runner) forwardedEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
+ ts := r.getTestServer()
+ forwarder := lsprpc.NewForwarder("tcp", ts.Addr, false, debug.NewInstance("", ""))
+ ts2 := servertest.NewPipeServer(ctx, forwarder)
+ cleanup := func() {
+ ts2.Close()
+ }
+ return ts2, cleanup
+}
+
+func (r *Runner) separateProcessEnv(ctx context.Context, t *testing.T) (servertest.Connector, func()) {
+ socket := r.getRemoteSocket(t)
+ // TODO(rfindley): can we use the autostart behavior here, instead of
+ // pre-starting the remote?
+ forwarder := lsprpc.NewForwarder("unix", socket, false, debug.NewInstance("", ""))
+ ts2 := servertest.NewPipeServer(ctx, forwarder)
+ cleanup := func() {
+ ts2.Close()
+ }
+ return ts2, cleanup
+}
+
+// Env holds an initialized fake Editor, Workspace, and Server, which may be
+// used for writing tests. It also provides adapter methods that call t.Fatal
+// on any error, so that tests for the happy path may be written without
+// checking errors.
+type Env struct {
+ t *testing.T
+ ctx context.Context
+
+ // Most tests should not need to access the workspace or editor, or server,
+ // but they are available if needed.
+ W *fake.Workspace
+ E *fake.Editor
+ Server servertest.Connector
+
+ // mu guards the fields below, for the purpose of checking conditions on
+ // every change to diagnostics.
+ mu sync.Mutex
+ // For simplicity, each waiter gets a unique ID.
+ nextWaiterID int
+ lastDiagnostics map[string]*protocol.PublishDiagnosticsParams
+ waiters map[int]*diagnosticCondition
+}
+
+// A diagnosticCondition is satisfied when all expectations are simultaneously
+// met. At that point, the 'met' channel is closed.
+type diagnosticCondition struct {
+ expectations []DiagnosticExpectation
+ met chan struct{}
+}
+
+// NewEnv creates a new test environment using the given workspace and gopls
+// server.
+func NewEnv(ctx context.Context, t *testing.T, ws *fake.Workspace, ts servertest.Connector) *Env {
+ t.Helper()
+ conn := ts.Connect(ctx)
+ editor, err := fake.NewConnectedEditor(ctx, ws, conn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ env := &Env{
+ t: t,
+ ctx: ctx,
+ W: ws,
+ E: editor,
+ Server: ts,
+ lastDiagnostics: make(map[string]*protocol.PublishDiagnosticsParams),
+ waiters: make(map[int]*diagnosticCondition),
+ }
+ env.E.Client().OnDiagnostics(env.onDiagnostics)
+ return env
+}
+
+// RemoveFileFromWorkspace deletes a file on disk but does nothing in the
+// editor. It calls t.Fatal on any error.
+func (e *Env) RemoveFileFromWorkspace(name string) {
+ e.t.Helper()
+ if err := e.W.RemoveFile(e.ctx, name); err != nil {
+ e.t.Fatal(err)
+ }
+}
+
+// OpenFile opens a file in the editor, calling t.Fatal on any error.
+func (e *Env) OpenFile(name string) {
+ e.t.Helper()
+ if err := e.E.OpenFile(e.ctx, name); err != nil {
+ e.t.Fatal(err)
+ }
+}
+
+// CreateBuffer creates a buffer in the editor, calling t.Fatal on any error.
+func (e *Env) CreateBuffer(name string, content string) {
+ e.t.Helper()
+ if err := e.E.CreateBuffer(e.ctx, name, content); err != nil {
+ e.t.Fatal(err)
+ }
+}
+
+// CloseBuffer closes an editor buffer, calling t.Fatal on any error.
+func (e *Env) CloseBuffer(name string) {
+ e.t.Helper()
+ if err := e.E.CloseBuffer(e.ctx, name); err != nil {
+ e.t.Fatal(err)
+ }
+}
+
+// EditBuffer applies edits to an editor buffer, calling t.Fatal on any error.
+func (e *Env) EditBuffer(name string, edits ...fake.Edit) {
+ e.t.Helper()
+ if err := e.E.EditBuffer(e.ctx, name, edits); err != nil {
+ e.t.Fatal(err)
+ }
+}
+
+// GoToDefinition goes to definition in the editor, calling t.Fatal on any
+// error.
+func (e *Env) GoToDefinition(name string, pos fake.Pos) (string, fake.Pos) {
+ e.t.Helper()
+ n, p, err := e.E.GoToDefinition(e.ctx, name, pos)
+ if err != nil {
+ e.t.Fatal(err)
+ }
+ return n, p
+}
+
+func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
+ pth := e.W.URIToPath(d.URI)
+ e.lastDiagnostics[pth] = d
+
+ for id, condition := range e.waiters {
+ if meetsCondition(e.lastDiagnostics, condition.expectations) {
+ delete(e.waiters, id)
+ close(condition.met)
+ }
+ }
+ return nil
+}
+
+// CloseEditor shuts down the editor, calling t.Fatal on any error.
+func (e *Env) CloseEditor() {
+ e.t.Helper()
+ if err := e.E.Shutdown(e.ctx); err != nil {
+ e.t.Fatal(err)
+ }
+ if err := e.E.Exit(e.ctx); err != nil {
+ e.t.Fatal(err)
+ }
+}
+
+func meetsCondition(m map[string]*protocol.PublishDiagnosticsParams, expectations []DiagnosticExpectation) bool {
+ for _, e := range expectations {
+ if !e.IsMet(m) {
+ return false
+ }
+ }
+ return true
+}
+
+// A DiagnosticExpectation is a condition that must be met by the current set
+// of diagnostics.
+type DiagnosticExpectation struct {
+ IsMet func(map[string]*protocol.PublishDiagnosticsParams) bool
+ Description string
+}
+
+// EmptyDiagnostics asserts that diagnostics are empty for the
+// workspace-relative path name.
+func EmptyDiagnostics(name string) DiagnosticExpectation {
+ isMet := func(diags map[string]*protocol.PublishDiagnosticsParams) bool {
+ ds, ok := diags[name]
+ return ok && len(ds.Diagnostics) == 0
+ }
+ return DiagnosticExpectation{
+ IsMet: isMet,
+ Description: fmt.Sprintf("empty diagnostics for %q", name),
+ }
+}
+
+// DiagnosticAt asserts that there is a diagnostic entry at the position
+// specified by line and col, for the workspace-relative path name.
+func DiagnosticAt(name string, line, col int) DiagnosticExpectation {
+ isMet := func(diags map[string]*protocol.PublishDiagnosticsParams) bool {
+ ds, ok := diags[name]
+ if !ok || len(ds.Diagnostics) == 0 {
+ return false
+ }
+ for _, d := range ds.Diagnostics {
+ if d.Range.Start.Line == float64(line) && d.Range.Start.Character == float64(col) {
+ return true
+ }
+ }
+ return false
+ }
+ return DiagnosticExpectation{
+ IsMet: isMet,
+ Description: fmt.Sprintf("diagnostic in %q at (line:%d, column:%d)", name, line, col),
+ }
+}
+
+// Await waits for all diagnostic expectations to simultaneously be met.
+func (e *Env) Await(expectations ...DiagnosticExpectation) {
+ // NOTE: in the future this mechanism extend beyond just diagnostics, for
+ // example by modifying IsMet to be a func(*Env) boo. However, that would
+ // require careful checking of conditions around every state change, so for
+ // now we just limit the scope to diagnostic conditions.
+
+ e.t.Helper()
+ e.mu.Lock()
+ // Before adding the waiter, we check if the condition is currently met to
+ // avoid a race where the condition was realized before Await was called.
+ if meetsCondition(e.lastDiagnostics, expectations) {
+ e.mu.Unlock()
+ return
+ }
+ met := make(chan struct{})
+ e.waiters[e.nextWaiterID] = &diagnosticCondition{
+ expectations: expectations,
+ met: met,
+ }
+ e.nextWaiterID++
+ e.mu.Unlock()
+
+ select {
+ case <-e.ctx.Done():
+ // Debugging an unmet expectation can be tricky, so we put some effort into
+ // nicely formatting the failure.
+ var descs []string
+ for _, e := range expectations {
+ descs = append(descs, e.Description)
+ }
+ e.mu.Lock()
+ diagString := formatDiagnostics(e.lastDiagnostics)
+ e.mu.Unlock()
+ e.t.Fatalf("waiting on (%s):\nerr:%v\ndiagnostics:\n%s", strings.Join(descs, ", "), e.ctx.Err(), diagString)
+ case <-met:
+ }
+}
+
+func formatDiagnostics(diags map[string]*protocol.PublishDiagnosticsParams) string {
+ var b strings.Builder
+ for name, params := range diags {
+ b.WriteString(name + ":\n")
+ for _, d := range params.Diagnostics {
+ b.WriteString(fmt.Sprintf("\t(%d, %d): %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Message))
+ }
+ }
+ return b.String()
+}
diff --git a/internal/lsp/regtest/reg_test.go b/internal/lsp/regtest/reg_test.go
new file mode 100644
index 0000000..9246ccb
--- /dev/null
+++ b/internal/lsp/regtest/reg_test.go
@@ -0,0 +1,53 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package regtest
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "testing"
+ "time"
+
+ "golang.org/x/tools/internal/lsp/cmd"
+ "golang.org/x/tools/internal/lsp/lsprpc"
+ "golang.org/x/tools/internal/tool"
+)
+
+var (
+ runSubprocessTests = flag.Bool("enable_gopls_subprocess_tests", false, "run regtests against a gopls subprocess")
+ goplsBinaryPath = flag.String("gopls_test_binary", "", "path to the gopls binary for use as a remote, for use with the -gopls_subprocess_testmode flag")
+)
+
+var runner *Runner
+
+func TestMain(m *testing.M) {
+ flag.Parse()
+ if os.Getenv("_GOPLS_TEST_BINARY_RUN_AS_GOPLS") == "true" {
+ tool.Main(context.Background(), cmd.New("gopls", "", nil, nil), os.Args[1:])
+ os.Exit(0)
+ }
+ resetExitFuncs := lsprpc.OverrideExitFuncsForTest()
+ defer resetExitFuncs()
+
+ const testTimeout = 60 * time.Second
+ if *runSubprocessTests {
+ goplsPath := *goplsBinaryPath
+ if goplsPath == "" {
+ var err error
+ goplsPath, err = os.Executable()
+ if err != nil {
+ panic(fmt.Sprintf("finding test binary path: %v", err))
+ }
+ }
+ runner = NewTestRunner(NormalModes|SeparateProcess, testTimeout, goplsPath)
+ } else {
+ runner = NewTestRunner(NormalModes, testTimeout, "")
+ }
+ code := m.Run()
+ runner.Close()
+ os.Exit(code)
+}
diff --git a/internal/lsp/regtest/shared_test.go b/internal/lsp/regtest/shared_test.go
new file mode 100644
index 0000000..565241b
--- /dev/null
+++ b/internal/lsp/regtest/shared_test.go
@@ -0,0 +1,67 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package regtest
+
+import (
+ "context"
+ "testing"
+
+ "golang.org/x/tools/internal/lsp/fake"
+)
+
+const sharedProgram = `
+-- go.mod --
+module mod
+
+go 1.12
+-- main.go --
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello World.")
+}`
+
+func runShared(t *testing.T, program string, testFunc func(ctx context.Context, t *testing.T, env1 *Env, env2 *Env)) {
+ // Only run these tests in forwarded modes.
+ modes := runner.Modes() & (Forwarded | SeparateProcess)
+ runner.RunInMode(modes, t, sharedProgram, func(ctx context.Context, t *testing.T, env1 *Env) {
+ // Create a second test session connected to the same workspace and server
+ // as the first.
+ env2 := NewEnv(ctx, t, env1.W, env1.Server)
+ testFunc(ctx, t, env1, env2)
+ })
+}
+
+func TestSimultaneousEdits(t *testing.T) {
+ t.Parallel()
+ runShared(t, exampleProgram, func(ctx context.Context, t *testing.T, env1 *Env, env2 *Env) {
+ // In editor #1, break fmt.Println as before.
+ edit1 := fake.NewEdit(5, 11, 5, 12, "")
+ env1.OpenFile("main.go")
+ env1.EditBuffer("main.go", edit1)
+ // In editor #2 remove the closing brace.
+ edit2 := fake.NewEdit(6, 0, 6, 1, "")
+ env2.OpenFile("main.go")
+ env2.EditBuffer("main.go", edit2)
+
+ // Now check that we got different diagnostics in each environment.
+ env1.Await(DiagnosticAt("main.go", 5, 5))
+ env2.Await(DiagnosticAt("main.go", 7, 0))
+ })
+}
+
+func TestShutdown(t *testing.T) {
+ t.Parallel()
+ runShared(t, sharedProgram, func(ctx context.Context, t *testing.T, env1 *Env, env2 *Env) {
+ env1.CloseEditor()
+ // Now make an edit in editor #2 to trigger diagnostics.
+ edit2 := fake.NewEdit(6, 0, 6, 1, "")
+ env2.OpenFile("main.go")
+ env2.EditBuffer("main.go", edit2)
+ env2.Await(DiagnosticAt("main.go", 7, 0))
+ })
+}
diff --git a/internal/lsp/rename.go b/internal/lsp/rename.go
index 04f0054..9fe59e9 100644
--- a/internal/lsp/rename.go
+++ b/internal/lsp/rename.go
@@ -9,24 +9,13 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
)
func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
- if fh.Identity().Kind != source.Go {
- return nil, nil
- }
-
edits, err := source.Rename(ctx, snapshot, fh, params.Position, params.NewName)
if err != nil {
return nil, err
@@ -46,20 +35,10 @@
}
func (s *Server) prepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.Range, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
- if fh.Identity().Kind != source.Go {
- return nil, nil
- }
-
// Do not return errors here, as it adds clutter.
// Returning a nil result means there is not a valid rename.
item, err := source.PrepareRename(ctx, snapshot, fh, params.Position)
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 9de3c06..b5bdb6f 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -7,61 +7,26 @@
import (
"context"
- "fmt"
- "net"
"sync"
"golang.org/x/tools/internal/jsonrpc2"
+ "golang.org/x/tools/internal/lsp/mod"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
-// NewClientServer
-func NewClientServer(ctx context.Context, session source.Session, client protocol.Client) (context.Context, *Server) {
- ctx = protocol.WithClient(ctx, client)
- return ctx, &Server{
- client: client,
- session: session,
- delivered: make(map[span.URI]sentDiagnostics),
- }
-}
+const concurrentAnalyses = 1
// NewServer creates an LSP server and binds it to handle incoming client
// messages on on the supplied stream.
-func NewServer(ctx context.Context, session source.Session, stream jsonrpc2.Stream) (context.Context, *Server) {
- s := &Server{
- delivered: make(map[span.URI]sentDiagnostics),
- session: session,
+func NewServer(session source.Session, client protocol.Client) *Server {
+ return &Server{
+ delivered: make(map[span.URI]sentDiagnostics),
+ session: session,
+ client: client,
+ diagnosticsSema: make(chan struct{}, concurrentAnalyses),
}
- ctx, s.Conn, s.client = protocol.NewServer(ctx, stream, s)
- return ctx, s
-}
-
-// RunServerOnPort starts an LSP server on the given port and does not exit.
-// This function exists for debugging purposes.
-func RunServerOnPort(ctx context.Context, cache source.Cache, port int, h func(ctx context.Context, s *Server)) error {
- return RunServerOnAddress(ctx, cache, fmt.Sprintf(":%v", port), h)
-}
-
-// RunServerOnAddress starts an LSP server on the given address and does not
-// exit. This function exists for debugging purposes.
-func RunServerOnAddress(ctx context.Context, cache source.Cache, addr string, h func(ctx context.Context, s *Server)) error {
- ln, err := net.Listen("tcp", addr)
- if err != nil {
- return err
- }
- for {
- conn, err := ln.Accept()
- if err != nil {
- return err
- }
- h(NewServer(ctx, cache.NewSession(), jsonrpc2.NewHeaderStream(conn, conn)))
- }
-}
-
-func (s *Server) Run(ctx context.Context) error {
- return s.Conn.Run(ctx)
}
type serverState int
@@ -73,8 +38,8 @@
serverShutDown
)
+// Server implements the protocol.Server interface.
type Server struct {
- Conn *jsonrpc2.Conn
client protocol.Client
stateMu sync.Mutex
@@ -92,6 +57,12 @@
// delivered is a cache of the diagnostics that the server has sent.
deliveredMu sync.Mutex
delivered map[span.URI]sentDiagnostics
+
+ showedInitialError bool
+ showedInitialErrorMu sync.Mutex
+
+ // diagnosticsSema limits the concurrency of diagnostics runs, which can be expensive.
+ diagnosticsSema chan struct{}
}
// sentDiagnostics is used to cache diagnostics that have been sent for a given file.
@@ -108,24 +79,31 @@
}
func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) {
- return nil, nil
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Mod)
+ if !ok {
+ return nil, err
+ }
+ if !snapshot.IsSaved(fh.Identity().URI) {
+ return nil, nil
+ }
+ return mod.CodeLens(ctx, snapshot, fh.Identity().URI)
}
func (s *Server) nonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) {
paramMap := params.(map[string]interface{})
if method == "gopls/diagnoseFiles" {
for _, file := range paramMap["files"].([]interface{}) {
- uri := span.URI(file.(string))
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(protocol.DocumentURI(file.(string)), source.UnknownKind)
+ if !ok {
return nil, err
}
- fileID, diagnostics, err := source.FileDiagnostics(ctx, view.Snapshot(), uri)
+
+ fileID, diagnostics, err := source.FileDiagnostics(ctx, snapshot, fh.Identity().URI)
if err != nil {
return nil, err
}
if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
- URI: protocol.NewURI(uri),
+ URI: protocol.URIFromSpanURI(fh.Identity().URI),
Diagnostics: toProtocolDiagnostics(diagnostics),
Version: fileID.Version,
}); err != nil {
diff --git a/internal/lsp/server_gen.go b/internal/lsp/server_gen.go
index ea5f8be..4205ad5 100644
--- a/internal/lsp/server_gen.go
+++ b/internal/lsp/server_gen.go
@@ -132,7 +132,7 @@
return nil, notImplemented("PrepareCallHierarchy")
}
-func (s *Server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (interface{}, error) {
+func (s *Server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.Range, error) {
return s.prepareRename(ctx, params)
}
@@ -192,8 +192,8 @@
return s.signatureHelp(ctx, params)
}
-func (s *Server) Symbol(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
- return nil, notImplemented("Symbol")
+func (s *Server) Symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
+ return s.symbol(ctx, params)
}
func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) (protocol.Definition, error) {
diff --git a/internal/lsp/signature_help.go b/internal/lsp/signature_help.go
index fab6453..a978fd8 100644
--- a/internal/lsp/signature_help.go
+++ b/internal/lsp/signature_help.go
@@ -9,53 +9,22 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/tag"
)
func (s *Server) signatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) {
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
return nil, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
- if err != nil {
- return nil, err
- }
- if fh.Identity().Kind != source.Go {
- return nil, nil
- }
- info, err := source.SignatureHelp(ctx, snapshot, fh, params.Position)
+ info, activeParameter, err := source.SignatureHelp(ctx, snapshot, fh, params.Position)
if err != nil {
log.Print(ctx, "no signature help", tag.Of("At", params.Position), tag.Of("Failure", err))
return nil, nil
}
- return toProtocolSignatureHelp(info), nil
-}
-
-func toProtocolSignatureHelp(info *source.SignatureInformation) *protocol.SignatureHelp {
return &protocol.SignatureHelp{
- ActiveParameter: float64(info.ActiveParameter),
- ActiveSignature: 0, // there is only ever one possible signature
- Signatures: []protocol.SignatureInformation{
- {
- Label: info.Label,
- Documentation: info.Documentation,
- Parameters: toProtocolParameterInformation(info.Parameters),
- },
- },
- }
-}
-
-func toProtocolParameterInformation(info []source.ParameterInformation) []protocol.ParameterInformation {
- var result []protocol.ParameterInformation
- for _, p := range info {
- result = append(result, protocol.ParameterInformation{
- Label: p.Label,
- })
- }
- return result
+ Signatures: []protocol.SignatureInformation{*info},
+ ActiveParameter: float64(activeParameter),
+ }, nil
}
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 03fe101..b9cab74 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -9,6 +9,7 @@
"fmt"
"go/ast"
"go/constant"
+ "go/scanner"
"go/token"
"go/types"
"math"
@@ -323,7 +324,16 @@
return
}
- if c.matchingCandidate(&cand, nil) {
+ // If we know we want a type name, don't offer non-type name
+ // candidates. However, do offer package names since they can
+ // contain type names, and do offer any candidate without a type
+ // since we aren't sure if it is a type name or not (i.e. unimported
+ // candidate).
+ if c.wantTypeName() && obj.Type() != nil && !isTypeName(obj) && !isPkgName(obj) {
+ return
+ }
+
+ if c.matchingCandidate(&cand) {
cand.score *= highScore
} else if isTypeName(obj) {
// If obj is a *types.TypeName that didn't otherwise match, check
@@ -383,8 +393,9 @@
// makePointer is true if the candidate type name T should be made into *T.
makePointer bool
- // dereference is true if the candidate obj should be made into *obj.
- dereference bool
+ // dereference is a count of how many times to dereference the candidate obj.
+ // For example, dereference=2 turns "foo" into "**foo" when formatting.
+ dereference int
// imp is the import that needs to be added to this package in order
// for this candidate to be valid. nil if no import needed.
@@ -422,7 +433,7 @@
if err != nil {
return nil, nil, fmt.Errorf("getting file for Completion: %v", err)
}
- file, m, _, err := pgh.Cached()
+ file, src, m, _, err := pgh.Cached()
if err != nil {
return nil, nil, err
}
@@ -481,9 +492,26 @@
c.deepState.maxDepth = -1
}
- // Set the filter surrounding.
- if ident, ok := path[0].(*ast.Ident); ok {
- c.setSurrounding(ident)
+ // Detect our surrounding identifier.
+ switch leaf := path[0].(type) {
+ case *ast.Ident:
+ // In the normal case, our leaf AST node is the identifier being completed.
+ c.setSurrounding(leaf)
+ case *ast.BadDecl:
+ // You don't get *ast.Idents at the file level, so look for bad
+ // decls and manually extract the surrounding token.
+ pos, _, lit := c.scanToken(ctx, src)
+ if pos.IsValid() {
+ c.setSurrounding(&ast.Ident{Name: lit, NamePos: pos})
+ }
+ default:
+ // Otherwise, manually extract the prefix if our containing token
+ // is a keyword. This improves completion after an "accidental
+ // keyword", e.g. completing to "variance" in "someFunc(var<>)".
+ pos, tkn, lit := c.scanToken(ctx, src)
+ if pos.IsValid() && tkn.IsKeyword() {
+ c.setSurrounding(&ast.Ident{Name: lit, NamePos: pos})
+ }
}
c.inference = expectedCandidate(c)
@@ -492,7 +520,7 @@
// If we're inside a comment return comment completions
for _, comment := range file.Comments {
- if comment.Pos() <= rng.Start && rng.Start <= comment.End() {
+ if comment.Pos() < rng.Start && rng.Start <= comment.End() {
c.populateCommentCompletions(comment)
return c.items, c.getSurrounding(), nil
}
@@ -536,10 +564,6 @@
if err := c.lexical(); err != nil {
return nil, nil, err
}
- if err := c.keyword(); err != nil {
- return nil, nil, err
- }
-
// The function name hasn't been typed yet, but the parens are there:
// recv.‸(arg)
case *ast.TypeAssertExpr:
@@ -549,19 +573,14 @@
}
case *ast.SelectorExpr:
- // The go parser inserts a phantom "_" Sel node when the selector is
- // not followed by an identifier or a "(". The "_" isn't actually in
- // the text, so don't think it is our surrounding.
- // TODO: Find a way to differentiate between phantom "_" and real "_",
- // perhaps by checking if "_" is present in file contents.
- if n.Sel.Name != "_" || c.pos != n.Sel.Pos() {
- c.setSurrounding(n.Sel)
- }
-
if err := c.selector(n); err != nil {
return nil, nil, err
}
+ // At the file scope, only keywords are allowed.
+ case *ast.BadDecl, *ast.File:
+ c.addKeywordCompletions()
+
default:
// fallback to lexical completions
if err := c.lexical(); err != nil {
@@ -572,6 +591,24 @@
return c.items, c.getSurrounding(), nil
}
+// scanToken scans pgh's contents for the token containing pos.
+func (c *completer) scanToken(ctx context.Context, contents []byte) (token.Pos, token.Token, string) {
+ tok := c.snapshot.View().Session().Cache().FileSet().File(c.pos)
+
+ var s scanner.Scanner
+ s.Init(tok, contents, nil, 0)
+ for {
+ tknPos, tkn, lit := s.Scan()
+ if tkn == token.EOF || tknPos >= c.pos {
+ return token.NoPos, token.ILLEGAL, ""
+ }
+
+ if len(lit) > 0 && tknPos <= c.pos && c.pos <= tknPos+token.Pos(len(lit)) {
+ return tknPos, tkn, lit
+ }
+ }
+}
+
func (c *completer) sortItems() {
sort.SliceStable(c.items, func(i, j int) bool {
// Sort by score first.
@@ -642,14 +679,17 @@
}
// See https://golang.org/issue/36001. Unimported completions are expensive.
-const unimportedTarget = 100
+const (
+ maxUnimportedPackageNames = 5
+ unimportedMemberTarget = 100
+)
// selector finds completions for the specified selector expression.
func (c *completer) selector(sel *ast.SelectorExpr) error {
// Is sel a qualified identifier?
if id, ok := sel.X.(*ast.Ident); ok {
- if pkgname, ok := c.pkg.GetTypesInfo().Uses[id].(*types.PkgName); ok {
- c.packageMembers(pkgname.Imported(), stdScore, nil)
+ if pkgName, ok := c.pkg.GetTypesInfo().Uses[id].(*types.PkgName); ok {
+ c.packageMembers(pkgName.Imported(), stdScore, nil)
return nil
}
}
@@ -661,7 +701,7 @@
}
// Try unimported packages.
- if id, ok := sel.X.(*ast.Ident); ok && c.opts.unimported && len(c.items) < unimportedTarget {
+ if id, ok := sel.X.(*ast.Ident); ok && c.opts.unimported {
if err := c.unimportedMembers(id); err != nil {
return err
}
@@ -675,6 +715,7 @@
if err != nil {
return err
}
+
var paths []string
for path, pkg := range known {
if pkg.GetTypes().Name() != id.Name {
@@ -682,6 +723,7 @@
}
paths = append(paths, path)
}
+
var relevances map[string]int
if len(paths) != 0 {
c.snapshot.View().RunProcessEnvFunc(c.ctx, func(opts *imports.Options) error {
@@ -689,6 +731,7 @@
return nil
})
}
+
for path, pkg := range known {
if pkg.GetTypes().Name() != id.Name {
continue
@@ -700,8 +743,8 @@
if imports.ImportPathToAssumedName(path) != pkg.GetTypes().Name() {
imp.name = pkg.GetTypes().Name()
}
- c.packageMembers(pkg.GetTypes(), .01*float64(relevances[path]), imp)
- if len(c.items) >= unimportedTarget {
+ c.packageMembers(pkg.GetTypes(), stdScore+.01*float64(relevances[path]), imp)
+ if len(c.items) >= unimportedMemberTarget {
return nil
}
}
@@ -719,7 +762,7 @@
// Continue with untyped proposals.
pkg := types.NewPackage(pkgExport.Fix.StmtInfo.ImportPath, pkgExport.Fix.IdentName)
for _, export := range pkgExport.Exports {
- score := 0.01 * float64(pkgExport.Fix.Relevance)
+ score := stdScore + 0.01*float64(pkgExport.Fix.Relevance)
c.found(candidate{
obj: types.NewVar(0, pkg, export, nil),
score: score,
@@ -729,7 +772,7 @@
},
})
}
- if len(c.items) >= unimportedTarget {
+ if len(c.items) >= unimportedMemberTarget {
cancel()
}
}
@@ -828,7 +871,7 @@
// If obj's type is invalid, find the AST node that defines the lexical block
// containing the declaration of obj. Don't resolve types for packages.
- if _, ok := obj.(*types.PkgName); !ok && !typeIsValid(obj.Type()) {
+ if !isPkgName(obj) && !typeIsValid(obj.Type()) {
// Match the scope to its ast.Node. If the scope is the package scope,
// use the *ast.File as the starting node.
var node ast.Node
@@ -909,7 +952,7 @@
}
}
- if c.opts.unimported && len(c.items) < unimportedTarget {
+ if c.opts.unimported {
ctx, cancel := c.deepCompletionContext()
defer cancel()
// Suggest packages that have not been imported yet.
@@ -917,13 +960,22 @@
if c.surrounding != nil {
prefix = c.surrounding.Prefix()
}
- var mu sync.Mutex
+ var (
+ mu sync.Mutex
+ initialItemCount = len(c.items)
+ )
add := func(pkg imports.ImportFix) {
mu.Lock()
defer mu.Unlock()
if _, ok := seen[pkg.IdentName]; ok {
return
}
+
+ if len(c.items)-initialItemCount >= maxUnimportedPackageNames {
+ cancel()
+ return
+ }
+
// Rank unimported packages significantly lower than other results.
score := 0.01 * float64(pkg.Relevance)
@@ -939,18 +991,6 @@
name: pkg.StmtInfo.Name,
},
})
-
- if len(c.items) >= unimportedTarget {
- cancel()
- }
- c.found(candidate{
- obj: obj,
- score: score,
- imp: &importInfo{
- importPath: pkg.StmtInfo.ImportPath,
- name: pkg.StmtInfo.Name,
- },
- })
}
if err := c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
return imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.GetTypes().Name(), opts)
@@ -970,6 +1010,9 @@
}
}
+ // Add keyword completion items appropriate in the current context.
+ c.addKeywordCompletions()
+
return nil
}
@@ -1256,9 +1299,10 @@
// objKind is a mask of expected kinds of types such as "map", "slice", etc.
objKind objKind
- // variadic is true if objType is a slice type from an initial
- // variadic param.
- variadic bool
+ // variadicType is the scalar variadic element type. For example,
+ // when completing "append([]T{}, <>)" objType is []T and
+ // variadicType is T.
+ variadicType types.Type
// modifiers are prefixes such as "*", "&" or "<-" that influence how
// a candidate type relates to the expected type.
@@ -1270,6 +1314,26 @@
// typeName holds information about the expected type name at
// position, if any.
typeName typeNameInference
+
+ // assignees are the types that would receive a function call's
+ // results at the position. For example:
+ //
+ // foo := 123
+ // foo, bar := <>
+ //
+ // at "<>", the assignees are [int, <invalid>].
+ assignees []types.Type
+
+ // variadicAssignees is true if we could be completing an inner
+ // function call that fills out an outer function call's variadic
+ // params. For example:
+ //
+ // func foo(int, ...string) {}
+ //
+ // foo(<>) // variadicAssignees=true
+ // foo(bar<>) // variadicAssignees=true
+ // foo(bar, baz<>) // variadicAssignees=false
+ variadicAssignees bool
}
// typeNameInference holds information about the expected type name at
@@ -1296,7 +1360,6 @@
if c.enclosingCompositeLiteral != nil {
inf.objType = c.expectedCompositeLiteralType()
- return inf
}
Nodes:
@@ -1322,6 +1385,20 @@
if tv, ok := c.pkg.GetTypesInfo().Types[node.Lhs[i]]; ok {
inf.objType = tv.Type
}
+
+ // If we have a single expression on the RHS, record the LHS
+ // assignees so we can favor multi-return function calls with
+ // matching result values.
+ if len(node.Rhs) <= 1 {
+ for _, lhs := range node.Lhs {
+ inf.assignees = append(inf.assignees, c.pkg.GetTypesInfo().TypeOf(lhs))
+ }
+ } else {
+ // Otherwse, record our single assignee, even if its type is
+ // not available. We use this info to downrank functions
+ // with the wrong number of result values.
+ inf.assignees = append(inf.assignees, c.pkg.GetTypesInfo().TypeOf(node.Lhs[i]))
+ }
}
return inf
case *ast.ValueSpec:
@@ -1352,12 +1429,28 @@
beyondLastParam = exprIdx >= numParams
)
+ // If we have one or zero arg expressions, we may be
+ // completing to a function call that returns multiple
+ // values, in turn getting passed in to the surrounding
+ // call. Record the assignees so we can favor function
+ // calls that return matching values.
+ if len(node.Args) <= 1 {
+ for i := 0; i < sig.Params().Len(); i++ {
+ inf.assignees = append(inf.assignees, sig.Params().At(i).Type())
+ }
+
+ // Record that we may be completing into variadic parameters.
+ inf.variadicAssignees = sig.Variadic()
+ }
+
if sig.Variadic() {
+ variadicType := deslice(sig.Params().At(numParams - 1).Type())
+
// If we are beyond the last param or we are the last
// param w/ further expressions, we expect a single
// variadic item.
if beyondLastParam || isLastParam && len(node.Args) > numParams {
- inf.objType = sig.Params().At(numParams - 1).Type().(*types.Slice).Elem()
+ inf.objType = variadicType
break Nodes
}
@@ -1365,7 +1458,7 @@
// completing the variadic positition (i.e. we expect a
// slice type []T or an individual item T).
if isLastParam {
- inf.variadic = true
+ inf.variadicType = variadicType
}
}
@@ -1375,8 +1468,6 @@
} else {
inf.objType = sig.Params().At(exprIdx).Type()
}
-
- break Nodes
}
}
@@ -1384,16 +1475,11 @@
obj := c.pkg.GetTypesInfo().ObjectOf(funIdent)
if obj != nil && obj.Parent() == types.Universe {
- inf.objKind |= c.builtinArgKind(obj, node)
-
- if obj.Name() == "new" {
- inf.typeName.wantTypeName = true
- }
-
// Defer call to builtinArgType so we can provide it the
// inferred type from its parent node.
defer func() {
- inf.objType, inf.variadic = c.builtinArgType(obj, node, inf.objType)
+ inf = c.builtinArgType(obj, node, inf)
+ inf.objKind = c.builtinArgKind(obj, node)
}()
// The expected type of builtin arguments like append() is
@@ -1473,7 +1559,6 @@
inf.modifiers = append(inf.modifiers, typeModifier{mod: address})
case token.ARROW:
inf.modifiers = append(inf.modifiers, typeModifier{mod: chanRead})
- inf.objKind |= kindChan
}
default:
if breaksExpectedTypeInference(node) {
@@ -1538,7 +1623,7 @@
// matchesVariadic returns true if we are completing a variadic
// parameter and candType is a compatible slice type.
func (ci candidateInference) matchesVariadic(candType types.Type) bool {
- return ci.variadic && types.AssignableTo(ci.objType, candType)
+ return ci.variadicType != nil && types.AssignableTo(ci.objType, candType)
}
@@ -1668,6 +1753,11 @@
wantComparable = c.pos == n.Pos()+token.Pos(len("map["))
}
break Nodes
+ case *ast.ValueSpec:
+ if n.Type != nil && n.Type.Pos() <= c.pos && c.pos <= n.Type.End() {
+ wantTypeName = true
+ }
+ break Nodes
default:
if breaksExpectedTypeInference(p) {
return typeNameInference{}
@@ -1687,10 +1777,81 @@
return types.NewVar(token.NoPos, c.pkg.GetTypes(), "", T)
}
-// matchingCandidate reports whether a candidate matches our type
-// inferences. seen is used to detect recursive types in certain cases
-// and should be set to nil when calling matchingCandidate.
-func (c *completer) matchingCandidate(cand *candidate, seen map[types.Type]struct{}) bool {
+// anyCandType reports whether f returns true for any candidate type
+// derivable from c. For example, from "foo" we might derive "&foo",
+// and "foo()".
+func (c *candidate) anyCandType(f func(t types.Type, addressable bool) bool) bool {
+ if c.obj == nil || c.obj.Type() == nil {
+ return false
+ }
+
+ objType := c.obj.Type()
+
+ if f(objType, c.addressable) {
+ return true
+ }
+
+ // If c is a func type with a single result, offer the result type.
+ if sig, ok := objType.Underlying().(*types.Signature); ok {
+ if sig.Results().Len() == 1 && f(sig.Results().At(0).Type(), false) {
+ // Mark the candidate so we know to append "()" when formatting.
+ c.expandFuncCall = true
+ return true
+ }
+ }
+
+ var (
+ seenPtrTypes map[types.Type]bool
+ ptrType = objType
+ ptrDepth int
+ )
+
+ // Check if dereferencing c would match our type inference. We loop
+ // since c could have arbitrary levels of pointerness.
+ for {
+ ptr, ok := ptrType.Underlying().(*types.Pointer)
+ if !ok {
+ break
+ }
+
+ ptrDepth++
+
+ // Avoid pointer type cycles.
+ if seenPtrTypes[ptrType] {
+ break
+ }
+
+ if _, named := ptrType.(*types.Named); named {
+ // Lazily allocate "seen" since it isn't used normally.
+ if seenPtrTypes == nil {
+ seenPtrTypes = make(map[types.Type]bool)
+ }
+
+ // Track named pointer types we have seen to detect cycles.
+ seenPtrTypes[ptrType] = true
+ }
+
+ if f(ptr.Elem(), false) {
+ // Mark the candidate so we know to prepend "*" when formatting.
+ c.dereference = ptrDepth
+ return true
+ }
+
+ ptrType = ptr.Elem()
+ }
+
+ // Check if c is addressable and a pointer to c matches our type inference.
+ if c.addressable && f(types.NewPointer(objType), false) {
+ // Mark the candidate so we know to prepend "&" when formatting.
+ c.takeAddress = true
+ return true
+ }
+
+ return false
+}
+
+// matchingCandidate reports whether cand matches our type inferences.
+func (c *completer) matchingCandidate(cand *candidate) bool {
if isTypeName(cand.obj) {
return c.matchingTypeName(cand)
} else if c.wantTypeName() {
@@ -1698,118 +1859,179 @@
return false
}
+ if c.inference.candTypeMatches(cand) {
+ return true
+ }
+
candType := cand.obj.Type()
if candType == nil {
return false
}
+ if sig, ok := candType.Underlying().(*types.Signature); ok {
+ if c.inference.assigneesMatch(cand, sig) {
+ // Invoke the candidate if its results are multi-assignable.
+ cand.expandFuncCall = true
+ return true
+ }
+ }
+
// Default to invoking *types.Func candidates. This is so function
// completions in an empty statement (or other cases with no expected type)
// are invoked by default.
cand.expandFuncCall = isFunc(cand.obj)
- typeMatches := func(expType, candType types.Type) bool {
- if expType == nil {
- // If we don't expect a specific type, check if we expect a particular
- // kind of object (map, slice, etc).
- if c.inference.objKind > 0 {
- return c.inference.objKind&candKind(candType) > 0
- }
+ return false
+}
- return false
- }
+// candTypeMatches reports whether cand makes a good completion
+// candidate given the candidate inference. cand's score may be
+// mutated to downrank the candidate in certain situations.
+func (ci *candidateInference) candTypeMatches(cand *candidate) bool {
+ expTypes := make([]types.Type, 0, 2)
+ if ci.objType != nil {
+ expTypes = append(expTypes, ci.objType)
+ }
+ if ci.variadicType != nil {
+ expTypes = append(expTypes, ci.variadicType)
+ }
+ return cand.anyCandType(func(candType types.Type, addressable bool) bool {
// Take into account any type modifiers on the expected type.
- candType = c.inference.applyTypeModifiers(candType, cand.addressable)
+ candType = ci.applyTypeModifiers(candType, addressable)
if candType == nil {
return false
}
- // Handle untyped values specially since AssignableTo gives false negatives
- // for them (see https://golang.org/issue/32146).
- if candBasic, ok := candType.Underlying().(*types.Basic); ok {
- if wantBasic, ok := expType.Underlying().(*types.Basic); ok {
- // Make sure at least one of them is untyped.
- if isUntyped(candType) || isUntyped(expType) {
- // Check that their constant kind (bool|int|float|complex|string) matches.
- // This doesn't take into account the constant value, so there will be some
- // false positives due to integer sign and overflow.
- if candBasic.Info()&types.IsConstType == wantBasic.Info()&types.IsConstType {
- // Lower candidate score if the types are not identical. This avoids
- // ranking untyped constants above candidates with an exact type
- // match. Don't lower score of builtin constants (e.g. "true").
- if !types.Identical(candType, expType) && cand.obj.Parent() != types.Universe {
- cand.score /= 2
- }
- return true
- }
+ if ci.convertibleTo != nil && types.ConvertibleTo(candType, ci.convertibleTo) {
+ return true
+ }
+
+ if len(expTypes) == 0 {
+ // If we have no expected type but were able to apply type
+ // modifiers to our candidate type, count that as a match. This
+ // handles cases like:
+ //
+ // var foo chan int
+ // <-fo<>
+ //
+ // There is no exected type at "<>", but we were able to apply
+ // the "<-" type modifier to "foo", so it matches.
+ if len(ci.modifiers) > 0 {
+ return true
+ }
+
+ // If we have no expected type, fall back to checking the
+ // expected "kind" of object, if available.
+ return ci.kindMatches(candType)
+ }
+
+ for _, expType := range expTypes {
+ matches, untyped := ci.typeMatches(expType, candType)
+ if !matches {
+ continue
+ }
+
+ // Lower candidate score for untyped conversions. This avoids
+ // ranking untyped constants above candidates with an exact type
+ // match. Don't lower score of builtin constants, e.g. "true".
+ if untyped && !types.Identical(candType, expType) && cand.obj.Parent() != types.Universe {
+ cand.score /= 2
+ }
+
+ return true
+ }
+
+ return false
+ })
+}
+
+// typeMatches reports whether an object of candType makes a good
+// completion candidate given the expected type expType. It also
+// returns a second bool which is true if both types are basic types
+// of the same kind, and at least one is untyped.
+func (ci *candidateInference) typeMatches(expType, candType types.Type) (bool, bool) {
+ // Handle untyped values specially since AssignableTo gives false negatives
+ // for them (see https://golang.org/issue/32146).
+ if candBasic, ok := candType.Underlying().(*types.Basic); ok {
+ if wantBasic, ok := expType.Underlying().(*types.Basic); ok {
+ // Make sure at least one of them is untyped.
+ if isUntyped(candType) || isUntyped(expType) {
+ // Check that their constant kind (bool|int|float|complex|string) matches.
+ // This doesn't take into account the constant value, so there will be some
+ // false positives due to integer sign and overflow.
+ if candBasic.Info()&types.IsConstType == wantBasic.Info()&types.IsConstType {
+ return true, true
}
}
}
-
- // AssignableTo covers the case where the types are equal, but also handles
- // cases like assigning a concrete type to an interface type.
- return types.AssignableTo(candType, expType)
}
- if typeMatches(c.inference.objType, candType) {
- // If obj's type matches, we don't want to expand to an invocation of obj.
- cand.expandFuncCall = false
- return true
+ // AssignableTo covers the case where the types are equal, but also handles
+ // cases like assigning a concrete type to an interface type.
+ return types.AssignableTo(candType, expType), false
+}
+
+// kindMatches reports whether candType's kind matches our expected
+// kind (e.g. slice, map, etc.).
+func (ci *candidateInference) kindMatches(candType types.Type) bool {
+ return ci.objKind&candKind(candType) > 0
+}
+
+// assigneesMatch reports whether an invocation of sig matches the
+// number and type of any assignees.
+func (ci *candidateInference) assigneesMatch(cand *candidate, sig *types.Signature) bool {
+ if len(ci.assignees) == 0 {
+ return false
}
- // Try using a function's return type as its type.
- if sig, ok := candType.Underlying().(*types.Signature); ok && sig.Results().Len() == 1 {
- if typeMatches(c.inference.objType, sig.Results().At(0).Type()) {
- // If obj's return value matches the expected type, we need to invoke obj
- // in the completion.
- cand.expandFuncCall = true
- return true
- }
+ // Uniresult functions are always usable and are handled by the
+ // normal, non-assignees type matching logic.
+ if sig.Results().Len() == 1 {
+ return false
}
- // When completing the variadic parameter, if the expected type is
- // []T then check candType against T.
- if c.inference.variadic {
- if slice, ok := c.inference.objType.(*types.Slice); ok && typeMatches(slice.Elem(), candType) {
- return true
- }
+ var numberOfResultsCouldMatch bool
+ if ci.variadicAssignees {
+ numberOfResultsCouldMatch = sig.Results().Len() >= len(ci.assignees)-1
+ } else {
+ numberOfResultsCouldMatch = sig.Results().Len() == len(ci.assignees)
}
- if c.inference.convertibleTo != nil && types.ConvertibleTo(candType, c.inference.convertibleTo) {
- return true
+ // If our signature doesn't return the right number of values, it's
+ // not a match, so downrank it. For example:
+ //
+ // var foo func() (int, int)
+ // a, b, c := <> // downrank "foo()" since it only returns two values
+ if !numberOfResultsCouldMatch {
+ cand.score /= 2
+ return false
}
- // Check if dereferencing cand would match our type inference.
- if ptr, ok := cand.obj.Type().Underlying().(*types.Pointer); ok {
- // Notice if we have already encountered this pointer type before.
- _, saw := seen[cand.obj.Type()]
+ // If at least one assignee has a valid type, and all valid
+ // assignees match the corresponding sig result value, the signature
+ // is a match.
+ allMatch := false
+ for i := 0; i < sig.Results().Len(); i++ {
+ var assignee types.Type
- if _, named := cand.obj.Type().(*types.Named); named {
- // Lazily allocate "seen" since it isn't used normally.
- if seen == nil {
- seen = make(map[types.Type]struct{})
+ // If we are completing into variadic parameters, deslice the
+ // expected variadic type.
+ if ci.variadicAssignees && i >= len(ci.assignees)-1 {
+ assignee = ci.assignees[len(ci.assignees)-1]
+ if elem := deslice(assignee); elem != nil {
+ assignee = elem
}
-
- // Track named pointer types we have seen to detect cycles.
- seen[cand.obj.Type()] = struct{}{}
+ } else {
+ assignee = ci.assignees[i]
}
- if !saw && c.matchingCandidate(&candidate{obj: c.fakeObj(ptr.Elem())}, seen) {
- // Mark the candidate so we know to prepend "*" when formatting.
- cand.dereference = true
- return true
+ allMatch, _ = ci.typeMatches(assignee, sig.Results().At(i).Type())
+ if !allMatch {
+ break
}
}
-
- // Check if cand is addressable and a pointer to cand matches our type inference.
- if cand.addressable && c.matchingCandidate(&candidate{obj: c.fakeObj(types.NewPointer(candType))}, seen) {
- // Mark the candidate so we know to prepend "&" when formatting.
- cand.takeAddress = true
- return true
- }
-
- return false
+ return allMatch
}
func (c *completer) matchingTypeName(cand *candidate) bool {
diff --git a/internal/lsp/source/completion_builtin.go b/internal/lsp/source/completion_builtin.go
index da2c5f8..d65eb8f 100644
--- a/internal/lsp/source/completion_builtin.go
+++ b/internal/lsp/source/completion_builtin.go
@@ -49,23 +49,25 @@
}
// builtinArgType infers the type of an argument to a builtin
-// function. "parentType" is the inferred type for the builtin call's
-// parent node.
-func (c *completer) builtinArgType(obj types.Object, call *ast.CallExpr, parentType types.Type) (infType types.Type, variadic bool) {
- exprIdx := exprAtPos(c.pos, call.Args)
+// function. parentInf is the inferred type info for the builtin
+// call's parent node.
+func (c *completer) builtinArgType(obj types.Object, call *ast.CallExpr, parentInf candidateInference) candidateInference {
+ var (
+ exprIdx = exprAtPos(c.pos, call.Args)
+ inf = candidateInference{}
+ )
switch obj.Name() {
case "append":
- // Check if we are completing the variadic append() param.
- variadic = exprIdx == 1 && len(call.Args) <= 2
- infType = parentType
+ inf.objType = parentInf.objType
- // If we are completing an individual element of the variadic
- // param, "deslice" the expected type.
- if !variadic && exprIdx > 0 {
- if slice, ok := parentType.(*types.Slice); ok {
- infType = slice.Elem()
- }
+ // Check if we are completing the variadic append() param.
+ if exprIdx == 1 && len(call.Args) <= 2 {
+ inf.variadicType = deslice(inf.objType)
+ } else if exprIdx > 0 {
+ // If we are completing an individual element of the variadic
+ // param, "deslice" the expected type.
+ inf.objType = deslice(inf.objType)
}
case "delete":
if exprIdx > 0 && len(call.Args) > 0 {
@@ -73,7 +75,7 @@
firstArgType := c.pkg.GetTypesInfo().TypeOf(call.Args[0])
if firstArgType != nil {
if mt, ok := firstArgType.Underlying().(*types.Map); ok {
- infType = mt.Key()
+ inf.objType = mt.Key()
}
}
}
@@ -88,22 +90,26 @@
// Fill in expected type of either arg if the other is already present.
if exprIdx == 1 && t1 != nil {
- infType = t1
+ inf.objType = t1
} else if exprIdx == 0 && t2 != nil {
- infType = t2
+ inf.objType = t2
}
case "new":
- if parentType != nil {
+ inf.typeName.wantTypeName = true
+ if parentInf.objType != nil {
// Expected type for "new" is the de-pointered parent type.
- if ptr, ok := parentType.Underlying().(*types.Pointer); ok {
- infType = ptr.Elem()
+ if ptr, ok := parentInf.objType.Underlying().(*types.Pointer); ok {
+ inf.objType = ptr.Elem()
}
}
case "make":
if exprIdx == 0 {
- infType = parentType
+ inf.typeName.wantTypeName = true
+ inf.objType = parentInf.objType
+ } else {
+ inf.objType = types.Typ[types.Int]
}
}
- return infType, variadic
+ return inf
}
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index c5e8bbd..600ad5c 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -134,8 +134,10 @@
var prefixOp string
if cand.takeAddress {
prefixOp = "&"
- } else if cand.makePointer || cand.dereference {
+ } else if cand.makePointer {
prefixOp = "*"
+ } else if cand.dereference > 0 {
+ prefixOp = strings.Repeat("*", cand.dereference)
}
if prefixOp != "" {
@@ -177,7 +179,7 @@
if !pos.IsValid() {
return item, nil
}
- uri := span.FileURI(pos.Filename)
+ uri := span.URIFromPath(pos.Filename)
// Find the source file of the candidate, starting from a package
// that should have it in its dependencies.
@@ -211,7 +213,7 @@
return nil, nil
}
- uri := span.FileURI(c.filename)
+ uri := span.URIFromPath(c.filename)
var ph ParseGoHandle
for _, h := range c.pkg.CompiledGoFiles() {
if h.File().Identity().URI == uri {
diff --git a/internal/lsp/source/completion_keywords.go b/internal/lsp/source/completion_keywords.go
index dcb5c95..4cb7117 100644
--- a/internal/lsp/source/completion_keywords.go
+++ b/internal/lsp/source/completion_keywords.go
@@ -4,8 +4,6 @@
"go/ast"
"golang.org/x/tools/internal/lsp/protocol"
-
- errors "golang.org/x/xerrors"
)
const (
@@ -36,22 +34,48 @@
VAR = "var"
)
-// keyword looks at the current scope of an *ast.Ident and recommends keywords
-func (c *completer) keyword() error {
- keywordScore := float64(0.9)
- if _, ok := c.path[0].(*ast.Ident); !ok {
- // TODO(golang/go#34009): Support keyword completion in any context
- return errors.Errorf("keywords are currently only recommended for identifiers")
- }
- // Track which keywords we've already determined are in a valid scope
- // Use score to order keywords by how close we are to where they are useful
- valid := make(map[string]float64)
+// addKeywordCompletions offers keyword candidates appropriate at the position.
+func (c *completer) addKeywordCompletions() {
+ const keywordScore = 0.9
- // only suggest keywords at the begnning of a statement
+ seen := make(map[string]bool)
+
+ // addKeywords dedupes and adds completion items for the specified
+ // keywords with the specified score.
+ addKeywords := func(score float64, kws ...string) {
+ for _, kw := range kws {
+ if seen[kw] {
+ continue
+ }
+ seen[kw] = true
+
+ if c.matcher.Score(kw) > 0 {
+ c.items = append(c.items, CompletionItem{
+ Label: kw,
+ Kind: protocol.KeywordCompletion,
+ InsertText: kw,
+ Score: score,
+ })
+ }
+ }
+ }
+
+ // If we are at the file scope, only offer decl keywords. We don't
+ // get *ast.Idents at the file scope because non-keyword identifiers
+ // turn into *ast.BadDecl, not *ast.Ident.
+ if len(c.path) == 1 || isASTFile(c.path[1]) {
+ addKeywords(keywordScore, TYPE, CONST, VAR, FUNC, IMPORT)
+ return
+ } else if _, ok := c.path[0].(*ast.Ident); !ok {
+ // Otherwise only offer keywords if the client is completing an identifier.
+ return
+ }
+
+ // Only suggest keywords if we are beginning a statement.
switch c.path[1].(type) {
case *ast.BlockStmt, *ast.CommClause, *ast.CaseClause, *ast.ExprStmt:
default:
- return nil
+ return
}
// Filter out keywords depending on scope
@@ -62,7 +86,7 @@
case *ast.CaseClause:
// only recommend "fallthrough" and "break" within the bodies of a case clause
if c.pos > node.Colon {
- valid[BREAK] = keywordScore
+ addKeywords(keywordScore, BREAK)
// "fallthrough" is only valid in switch statements.
// A case clause is always nested within a block statement in a switch statement,
// that block statement is nested within either a TypeSwitchStmt or a SwitchStmt.
@@ -70,45 +94,23 @@
continue
}
if _, ok := path[i+2].(*ast.SwitchStmt); ok {
- valid[FALLTHROUGH] = keywordScore
+ addKeywords(keywordScore, FALLTHROUGH)
}
}
case *ast.CommClause:
if c.pos > node.Colon {
- valid[BREAK] = keywordScore
+ addKeywords(keywordScore, BREAK)
}
case *ast.TypeSwitchStmt, *ast.SelectStmt, *ast.SwitchStmt:
- valid[CASE] = keywordScore + lowScore
- valid[DEFAULT] = keywordScore + lowScore
+ addKeywords(keywordScore+lowScore, CASE, DEFAULT)
case *ast.ForStmt:
- valid[BREAK] = keywordScore
- valid[CONTINUE] = keywordScore
+ addKeywords(keywordScore, BREAK, CONTINUE)
// This is a bit weak, functions allow for many keywords
case *ast.FuncDecl:
if node.Body != nil && c.pos > node.Body.Lbrace {
- valid[DEFER] = keywordScore - lowScore
- valid[RETURN] = keywordScore - lowScore
- valid[FOR] = keywordScore - lowScore
- valid[GO] = keywordScore - lowScore
- valid[SWITCH] = keywordScore - lowScore
- valid[SELECT] = keywordScore - lowScore
- valid[IF] = keywordScore - lowScore
- valid[ELSE] = keywordScore - lowScore
- valid[VAR] = keywordScore - lowScore
- valid[CONST] = keywordScore - lowScore
+ addKeywords(keywordScore-lowScore, DEFER, RETURN, FOR, GO, SWITCH, SELECT, IF, ELSE, VAR, CONST, GOTO, TYPE)
}
}
}
- for ident, score := range valid {
- if c.matcher.Score(ident) > 0 {
- c.items = append(c.items, CompletionItem{
- Label: ident,
- Kind: protocol.KeywordCompletion,
- InsertText: ident,
- Score: score,
- })
- }
- }
- return nil
}
diff --git a/internal/lsp/source/completion_literal.go b/internal/lsp/source/completion_literal.go
index b95974e..8c9917c 100644
--- a/internal/lsp/source/completion_literal.go
+++ b/internal/lsp/source/completion_literal.go
@@ -26,7 +26,7 @@
expType := c.inference.objType
- if c.inference.variadic {
+ if c.inference.variadicType != nil {
// Don't offer literal slice candidates for variadic arguments.
// For example, don't offer "[]interface{}{}" in "fmt.Print(<>)".
if c.inference.matchesVariadic(literalType) {
@@ -35,9 +35,7 @@
// Otherwise, consider our expected type to be the variadic
// element type, not the slice type.
- if slice, ok := expType.(*types.Slice); ok {
- expType = slice.Elem()
- }
+ expType = c.inference.variadicType
}
// Avoid literal candidates if the expected type is an empty
@@ -75,7 +73,7 @@
cand.addressable = true
}
- if !c.matchingCandidate(&cand, nil) {
+ if !c.matchingCandidate(&cand) {
return
}
diff --git a/internal/lsp/source/deep_completion.go b/internal/lsp/source/deep_completion.go
index 74f2bbf..674ce49 100644
--- a/internal/lsp/source/deep_completion.go
+++ b/internal/lsp/source/deep_completion.go
@@ -138,7 +138,7 @@
return false
}
-// deepSearch searches through obj's subordinate objects for more
+// deepSearch searches through cand's subordinate objects for more
// completion items.
func (c *completer) deepSearch(cand candidate) {
if c.deepState.maxDepth == 0 {
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index 9e04898..ee81f53 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -6,8 +6,12 @@
import (
"context"
+ "fmt"
+ "go/ast"
+ "strconv"
"strings"
+ "golang.org/x/mod/modfile"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/telemetry"
@@ -39,7 +43,7 @@
Message string
}
-func Diagnostics(ctx context.Context, snapshot Snapshot, ph PackageHandle, withAnalysis bool) (map[FileIdentity][]Diagnostic, bool, error) {
+func Diagnostics(ctx context.Context, snapshot Snapshot, ph PackageHandle, missingModules map[string]*modfile.Require, withAnalysis bool) (map[FileIdentity][]Diagnostic, bool, error) {
// If we are missing dependencies, it may because the user's workspace is
// not correctly configured. Report errors, if possible.
var warn bool
@@ -56,12 +60,41 @@
if len(pkg.CompiledGoFiles()) == 1 && hasUndeclaredErrors(pkg) {
warn = true
}
+
+ isMissingModule := false
+ for _, imp := range pkg.Imports() {
+ if _, ok := missingModules[imp.PkgPath()]; ok {
+ isMissingModule = true
+ continue
+ }
+ for dep, req := range missingModules {
+ // If the import is a package of the dependency, then add the package to the map, this will
+ // eliminate the need to do this prefix package search on each import for each file.
+ // Example:
+ // import (
+ // "golang.org/x/tools/go/expect"
+ // "golang.org/x/tools/go/packages"
+ // )
+ // They both are related to the same module: "golang.org/x/tools"
+ if req != nil && strings.HasPrefix(imp.PkgPath(), dep) {
+ missingModules[imp.PkgPath()] = req
+ isMissingModule = true
+ break
+ }
+ }
+ }
+
// Prepare the reports we will send for the files in this package.
reports := make(map[FileIdentity][]Diagnostic)
for _, fh := range pkg.CompiledGoFiles() {
if err := clearReports(snapshot, reports, fh.File().Identity().URI); err != nil {
return nil, warn, err
}
+ if isMissingModule {
+ if err := missingModulesDiagnostics(ctx, snapshot, reports, missingModules, fh.File().Identity().URI); err != nil {
+ return nil, warn, err
+ }
+ }
}
// Prepare any additional reports for the errors in this package.
for _, e := range pkg.GetErrors() {
@@ -72,7 +105,7 @@
// If no file is associated with the error, pick an open file from the package.
if e.URI.Filename() == "" {
for _, ph := range pkg.CompiledGoFiles() {
- if snapshot.View().Session().IsOpen(ph.File().Identity().URI) {
+ if snapshot.IsOpen(ph.File().Identity().URI) {
e.URI = ph.File().Identity().URI
}
}
@@ -87,9 +120,13 @@
return nil, warn, err
}
if !hadDiagnostics && withAnalysis {
+ // Exit early if the context has been canceled. This also protects us
+ // from a race on Options, see golang/go#36699.
+ if ctx.Err() != nil {
+ return nil, warn, ctx.Err()
+ }
// If we don't have any list, parse, or type errors, run analyses.
if err := analyses(ctx, snapshot, reports, ph, snapshot.View().Options().DisabledAnalyses); err != nil {
- // Exit early if the context has been canceled.
if ctx.Err() != nil {
return nil, warn, ctx.Err()
}
@@ -112,7 +149,7 @@
if err != nil {
return FileIdentity{}, nil, err
}
- reports, _, err := Diagnostics(ctx, snapshot, ph, true)
+ reports, _, err := Diagnostics(ctx, snapshot, ph, nil, true)
if err != nil {
return FileIdentity{}, nil, err
}
@@ -178,6 +215,62 @@
return nonEmptyDiagnostics, nil
}
+func missingModulesDiagnostics(ctx context.Context, snapshot Snapshot, reports map[FileIdentity][]Diagnostic, missingModules map[string]*modfile.Require, uri span.URI) error {
+ if snapshot.View().Ignore(uri) || len(missingModules) == 0 {
+ return nil
+ }
+ fh, err := snapshot.GetFile(uri)
+ if err != nil {
+ return err
+ }
+ file, _, m, _, err := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseHeader).Parse(ctx)
+ if err != nil {
+ log.Error(ctx, "could not parse go file when checking for missing modules", err)
+ return err
+ }
+ // Make a dependency->import map to improve performance when finding missing dependencies.
+ imports := make(map[string]*ast.ImportSpec)
+ for _, imp := range file.Imports {
+ if imp.Path == nil {
+ continue
+ }
+ if target, err := strconv.Unquote(imp.Path.Value); err == nil {
+ imports[target] = imp
+ }
+ }
+ // If the go file has 0 imports, then we do not need to check for missing dependencies.
+ if len(imports) == 0 {
+ return nil
+ }
+ if reports[fh.Identity()] == nil {
+ reports[fh.Identity()] = []Diagnostic{}
+ }
+ for mod, req := range missingModules {
+ if req.Syntax == nil {
+ continue
+ }
+ imp, ok := imports[mod]
+ if !ok {
+ continue
+ }
+ spn, err := span.NewRange(snapshot.View().Session().Cache().FileSet(), imp.Path.Pos(), imp.Path.End()).Span()
+ if err != nil {
+ return err
+ }
+ rng, err := m.Range(spn)
+ if err != nil {
+ return err
+ }
+ reports[fh.Identity()] = append(reports[fh.Identity()], Diagnostic{
+ Message: fmt.Sprintf("%s is not in your go.mod file.", req.Mod.Path),
+ Range: rng,
+ Source: "go mod tidy",
+ Severity: protocol.SeverityWarning,
+ })
+ }
+ return nil
+}
+
func analyses(ctx context.Context, snapshot Snapshot, reports map[FileIdentity][]Diagnostic, ph PackageHandle, disabledAnalyses map[string]struct{}) error {
var analyzers []*analysis.Analyzer
for _, a := range snapshot.View().Options().Analyzers {
@@ -262,7 +355,7 @@
}
}
}
- return true
+ return len(fixes) > 0
}
// hasUndeclaredErrors returns true if a package has a type error
diff --git a/internal/lsp/source/errors.go b/internal/lsp/source/errors.go
deleted file mode 100644
index 5858603..0000000
--- a/internal/lsp/source/errors.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package source
-
-import (
- "bytes"
- "context"
- "fmt"
- "os/exec"
-
- errors "golang.org/x/xerrors"
-)
-
-// InvokeGo returns the output of a go command invocation.
-// It does not try to recover from errors.
-func InvokeGo(ctx context.Context, dir string, env []string, args ...string) (*bytes.Buffer, error) {
- stdout := new(bytes.Buffer)
- stderr := new(bytes.Buffer)
- cmd := exec.CommandContext(ctx, "go", args...)
- // On darwin the cwd gets resolved to the real path, which breaks anything that
- // expects the working directory to keep the original path, including the
- // go command when dealing with modules.
- // The Go stdlib has a special feature where if the cwd and the PWD are the
- // same node then it trusts the PWD, so by setting it in the env for the child
- // process we fix up all the paths returned by the go command.
- cmd.Env = append(append([]string{}, env...), "PWD="+dir)
- cmd.Dir = dir
- cmd.Stdout = stdout
- cmd.Stderr = stderr
-
- if err := cmd.Run(); err != nil {
- // Check for 'go' executable not being found.
- if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
- return nil, fmt.Errorf("'gopls requires 'go', but %s", exec.ErrNotFound)
- }
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
- return stdout, errors.Errorf("err: %v: stderr: %s", err, stderr)
- }
- return stdout, nil
-}
diff --git a/internal/lsp/source/folding_range.go b/internal/lsp/source/folding_range.go
index e7b06e3..21cfda1 100644
--- a/internal/lsp/source/folding_range.go
+++ b/internal/lsp/source/folding_range.go
@@ -19,7 +19,7 @@
// TODO(suzmue): consider limiting the number of folding ranges returned, and
// implement a way to prioritize folding ranges in that case.
pgh := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull)
- file, m, _, err := pgh.Parse(ctx)
+ file, _, m, _, err := pgh.Parse(ctx)
if err != nil {
return nil, err
}
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index 2149fea..d8efad0 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -28,7 +28,7 @@
defer done()
pgh := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseFull)
- file, m, parseErrors, err := pgh.Parse(ctx)
+ file, _, m, parseErrors, err := pgh.Parse(ctx)
if err != nil {
return nil, err
}
@@ -80,13 +80,10 @@
ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes")
defer done()
- pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle)
+ _, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle)
if err != nil {
return nil, nil, errors.Errorf("getting file for AllImportsFixes: %v", err)
}
- if hasListErrors(pkg) {
- return nil, nil, errors.Errorf("%s has list errors, not running goimports", fh.Identity().URI)
- }
err = snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
allFixEdits, editsPerFix, err = computeImportEdits(ctx, snapshot.View(), pgh, opts)
return err
@@ -108,7 +105,7 @@
if err != nil {
return nil, nil, err
}
- origAST, origMapper, _, err := ph.Parse(ctx)
+ origAST, _, origMapper, _, err := ph.Parse(ctx)
if err != nil {
return nil, nil, err
}
@@ -144,7 +141,7 @@
if err != nil {
return nil, err
}
- origAST, origMapper, _, err := ph.Parse(ctx)
+ origAST, _, origMapper, _, err := ph.Parse(ctx)
if err != nil {
return nil, err
}
@@ -297,15 +294,6 @@
return src[0:fset.Position(end).Offset], true
}
-func hasListErrors(pkg Package) bool {
- for _, err := range pkg.GetErrors() {
- if err.Kind == ListError {
- return true
- }
- }
- return false
-}
-
func computeTextEdits(ctx context.Context, view View, fh FileHandle, m *protocol.ColumnMapper, formatted string) ([]protocol.TextEdit, error) {
ctx, done := trace.StartSpan(ctx, "source.computeTextEdits")
defer done()
diff --git a/internal/lsp/source/highlight.go b/internal/lsp/source/highlight.go
index 07a2ac6..6b78fa4 100644
--- a/internal/lsp/source/highlight.go
+++ b/internal/lsp/source/highlight.go
@@ -27,7 +27,7 @@
if err != nil {
return nil, fmt.Errorf("getting file for Highlight: %v", err)
}
- file, m, _, err := pgh.Parse(ctx)
+ file, _, m, _, err := pgh.Parse(ctx)
if err != nil {
return nil, err
}
diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go
index 43bcd1b..2135506 100644
--- a/internal/lsp/source/hover.go
+++ b/internal/lsp/source/hover.go
@@ -44,6 +44,32 @@
comment *ast.CommentGroup
}
+func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
+ ident, err := Identifier(ctx, snapshot, fh, position)
+ if err != nil {
+ return nil, nil
+ }
+ h, err := ident.Hover(ctx)
+ if err != nil {
+ return nil, err
+ }
+ rng, err := ident.Range()
+ if err != nil {
+ return nil, err
+ }
+ hover, err := FormatHover(h, snapshot.View().Options())
+ if err != nil {
+ return nil, err
+ }
+ return &protocol.Hover{
+ Contents: protocol.MarkupContent{
+ Kind: snapshot.View().Options().PreferredContentFormat,
+ Value: hover,
+ },
+ Range: rng,
+ }, nil
+}
+
func (i *IdentifierInfo) Hover(ctx context.Context) (*HoverInformation, error) {
ctx, done := trace.StartSpan(ctx, "source.Hover")
defer done()
@@ -81,10 +107,22 @@
}
switch obj := obj.(type) {
case *types.PkgName:
- return obj.Imported().Path(), obj.Name()
+ path := obj.Imported().Path()
+ if mod, version, ok := moduleAtVersion(path, i); ok {
+ path = strings.Replace(path, mod, mod+"@"+version, 1)
+ }
+ return path, obj.Name()
case *types.Builtin:
return fmt.Sprintf("builtin#%s", obj.Name()), obj.Name()
}
+ // Check if the identifier is test-only (and is therefore not part of a
+ // package's API). This is true if the request originated in a test package,
+ // and if the declaration is also found in the same test package.
+ if i.pkg != nil && obj.Pkg() != nil && i.pkg.ForTest() != "" {
+ if _, pkg, _ := FindFileInPackage(i.pkg, i.Declaration.URI()); i.pkg == pkg {
+ return "", ""
+ }
+ }
// Don't return links for other unexported types.
if !obj.Exported() {
return "", ""
@@ -120,13 +158,35 @@
}
}
}
+ path := obj.Pkg().Path()
+ if mod, version, ok := moduleAtVersion(path, i); ok {
+ path = strings.Replace(path, mod, mod+"@"+version, 1)
+ }
if rTypeName != "" {
- link := fmt.Sprintf("%s#%s.%s", obj.Pkg().Path(), rTypeName, obj.Name())
+ link := fmt.Sprintf("%s#%s.%s", path, rTypeName, obj.Name())
symbol := fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), rTypeName, obj.Name())
return link, symbol
}
// For most cases, the link is "package/path#symbol".
- return fmt.Sprintf("%s#%s", obj.Pkg().Path(), obj.Name()), fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name())
+ return fmt.Sprintf("%s#%s", path, obj.Name()), fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name())
+}
+
+func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) {
+ if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" {
+ return "", "", false
+ }
+ impPkg, err := i.pkg.GetImport(path)
+ if err != nil {
+ return "", "", false
+ }
+ if impPkg.Module() == nil {
+ return "", "", false
+ }
+ version, modpath := impPkg.Module().Version, impPkg.Module().Path
+ if modpath == "" || version == "" {
+ return "", "", false
+ }
+ return modpath, version, true
}
// objectString is a wrapper around the types.ObjectString function.
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go
index 2694304..afd0b3f 100644
--- a/internal/lsp/source/identifier.go
+++ b/internal/lsp/source/identifier.go
@@ -48,15 +48,15 @@
// Identifier returns identifier information for a position
// in a file, accounting for a potentially incomplete selector.
-func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position, selectPackage PackagePolicy) (*IdentifierInfo, error) {
+func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (*IdentifierInfo, error) {
ctx, done := trace.StartSpan(ctx, "source.Identifier")
defer done()
- pkg, pgh, err := getParsedFile(ctx, snapshot, fh, selectPackage)
+ pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle)
if err != nil {
return nil, fmt.Errorf("getting file for Identifier: %v", err)
}
- file, m, _, err := pgh.Cached()
+ file, _, m, _, err := pgh.Cached()
if err != nil {
return nil, err
}
diff --git a/internal/lsp/source/implementation.go b/internal/lsp/source/implementation.go
index b2db818..1eceb8a 100644
--- a/internal/lsp/source/implementation.go
+++ b/internal/lsp/source/implementation.go
@@ -39,7 +39,7 @@
return nil, err
}
locations = append(locations, protocol.Location{
- URI: protocol.NewURI(rng.URI()),
+ URI: protocol.URIFromSpanURI(rng.URI()),
Range: pr,
})
}
@@ -241,7 +241,7 @@
return nil, 0, err
}
- file, m, _, err := pgh.Cached()
+ file, _, m, _, err := pgh.Cached()
if err != nil {
return nil, 0, err
}
@@ -299,7 +299,7 @@
}
}
case *ast.StarExpr:
- // Follow star expressions to the inner identifer.
+ // Follow star expressions to the inner identifier.
if pos == n.Star {
pos = n.X.Pos()
}
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index 61ab76a..cf0783a 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -44,60 +44,55 @@
errors "golang.org/x/xerrors"
)
-var (
- DefaultOptions = Options{
- ClientOptions: DefaultClientOptions,
- ServerOptions: DefaultServerOptions,
- UserOptions: DefaultUserOptions,
- DebuggingOptions: DefaultDebuggingOptions,
- ExperimentalOptions: DefaultExperimentalOptions,
- Hooks: DefaultHooks,
- }
- DefaultClientOptions = ClientOptions{
- InsertTextFormat: protocol.PlainTextTextFormat,
- PreferredContentFormat: protocol.Markdown,
- ConfigurationSupported: true,
- DynamicConfigurationSupported: true,
- DynamicWatchedFilesSupported: true,
- LineFoldingOnly: false,
- }
- DefaultServerOptions = ServerOptions{
- SupportedCodeActions: map[FileKind]map[protocol.CodeActionKind]bool{
- Go: {
- protocol.SourceOrganizeImports: true,
- protocol.QuickFix: true,
- },
- Mod: {
- protocol.SourceOrganizeImports: true,
- },
- Sum: {},
+func DefaultOptions() Options {
+ return Options{
+ ClientOptions: ClientOptions{
+ InsertTextFormat: protocol.PlainTextTextFormat,
+ PreferredContentFormat: protocol.Markdown,
+ ConfigurationSupported: true,
+ DynamicConfigurationSupported: true,
+ DynamicWatchedFilesSupported: true,
+ LineFoldingOnly: false,
},
- SupportedCommands: []string{
- "tidy", // for go.mod files
+ ServerOptions: ServerOptions{
+ SupportedCodeActions: map[FileKind]map[protocol.CodeActionKind]bool{
+ Go: {
+ protocol.SourceOrganizeImports: true,
+ protocol.QuickFix: true,
+ },
+ Mod: {
+ protocol.SourceOrganizeImports: true,
+ },
+ Sum: {},
+ },
+ SupportedCommands: []string{
+ "tidy", // for go.mod files
+ "upgrade.dependency", // for go.mod dependency upgrades
+ },
+ },
+ UserOptions: UserOptions{
+ Env: os.Environ(),
+ HoverKind: FullDocumentation,
+ LinkTarget: "pkg.go.dev",
+ Matcher: Fuzzy,
+ DeepCompletion: true,
+ UnimportedCompletion: true,
+ CompletionDocumentation: true,
+ },
+ DebuggingOptions: DebuggingOptions{
+ CompletionBudget: 100 * time.Millisecond,
+ },
+ ExperimentalOptions: ExperimentalOptions{
+ TempModfile: true,
+ },
+ Hooks: Hooks{
+ ComputeEdits: myers.ComputeEdits,
+ URLRegexp: regexp.MustCompile(`(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?`),
+ Analyzers: defaultAnalyzers(),
+ GoDiff: true,
},
}
- DefaultUserOptions = UserOptions{
- Env: os.Environ(),
- HoverKind: SynopsisDocumentation,
- LinkTarget: "pkg.go.dev",
- Matcher: Fuzzy,
- DeepCompletion: true,
- UnimportedCompletion: true,
- CompletionDocumentation: true,
- }
- DefaultHooks = Hooks{
- ComputeEdits: myers.ComputeEdits,
- URLRegexp: regexp.MustCompile(`(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?`),
- Analyzers: defaultAnalyzers,
- GoDiff: true,
- }
- DefaultExperimentalOptions = ExperimentalOptions{
- TempModfile: false,
- }
- DefaultDebuggingOptions = DebuggingOptions{
- CompletionBudget: 100 * time.Millisecond,
- }
-)
+}
type Options struct {
ClientOptions
@@ -464,34 +459,36 @@
}
}
-var defaultAnalyzers = map[string]*analysis.Analyzer{
- // The traditional vet suite:
- asmdecl.Analyzer.Name: asmdecl.Analyzer,
- assign.Analyzer.Name: assign.Analyzer,
- atomic.Analyzer.Name: atomic.Analyzer,
- atomicalign.Analyzer.Name: atomicalign.Analyzer,
- bools.Analyzer.Name: bools.Analyzer,
- buildtag.Analyzer.Name: buildtag.Analyzer,
- cgocall.Analyzer.Name: cgocall.Analyzer,
- composite.Analyzer.Name: composite.Analyzer,
- copylock.Analyzer.Name: copylock.Analyzer,
- errorsas.Analyzer.Name: errorsas.Analyzer,
- httpresponse.Analyzer.Name: httpresponse.Analyzer,
- loopclosure.Analyzer.Name: loopclosure.Analyzer,
- lostcancel.Analyzer.Name: lostcancel.Analyzer,
- nilfunc.Analyzer.Name: nilfunc.Analyzer,
- printf.Analyzer.Name: printf.Analyzer,
- shift.Analyzer.Name: shift.Analyzer,
- stdmethods.Analyzer.Name: stdmethods.Analyzer,
- structtag.Analyzer.Name: structtag.Analyzer,
- tests.Analyzer.Name: tests.Analyzer,
- unmarshal.Analyzer.Name: unmarshal.Analyzer,
- unreachable.Analyzer.Name: unreachable.Analyzer,
- unsafeptr.Analyzer.Name: unsafeptr.Analyzer,
- unusedresult.Analyzer.Name: unusedresult.Analyzer,
+func defaultAnalyzers() map[string]*analysis.Analyzer {
+ return map[string]*analysis.Analyzer{
+ // The traditional vet suite:
+ asmdecl.Analyzer.Name: asmdecl.Analyzer,
+ assign.Analyzer.Name: assign.Analyzer,
+ atomic.Analyzer.Name: atomic.Analyzer,
+ atomicalign.Analyzer.Name: atomicalign.Analyzer,
+ bools.Analyzer.Name: bools.Analyzer,
+ buildtag.Analyzer.Name: buildtag.Analyzer,
+ cgocall.Analyzer.Name: cgocall.Analyzer,
+ composite.Analyzer.Name: composite.Analyzer,
+ copylock.Analyzer.Name: copylock.Analyzer,
+ errorsas.Analyzer.Name: errorsas.Analyzer,
+ httpresponse.Analyzer.Name: httpresponse.Analyzer,
+ loopclosure.Analyzer.Name: loopclosure.Analyzer,
+ lostcancel.Analyzer.Name: lostcancel.Analyzer,
+ nilfunc.Analyzer.Name: nilfunc.Analyzer,
+ printf.Analyzer.Name: printf.Analyzer,
+ shift.Analyzer.Name: shift.Analyzer,
+ stdmethods.Analyzer.Name: stdmethods.Analyzer,
+ structtag.Analyzer.Name: structtag.Analyzer,
+ tests.Analyzer.Name: tests.Analyzer,
+ unmarshal.Analyzer.Name: unmarshal.Analyzer,
+ unreachable.Analyzer.Name: unreachable.Analyzer,
+ unsafeptr.Analyzer.Name: unsafeptr.Analyzer,
+ unusedresult.Analyzer.Name: unusedresult.Analyzer,
- // Non-vet analyzers
- deepequalerrors.Analyzer.Name: deepequalerrors.Analyzer,
- sortslice.Analyzer.Name: sortslice.Analyzer,
- testinggoroutine.Analyzer.Name: testinggoroutine.Analyzer,
+ // Non-vet analyzers
+ deepequalerrors.Analyzer.Name: deepequalerrors.Analyzer,
+ sortslice.Analyzer.Name: sortslice.Analyzer,
+ testinggoroutine.Analyzer.Name: testinggoroutine.Analyzer,
+ }
}
diff --git a/internal/lsp/source/signature_help.go b/internal/lsp/source/signature_help.go
index 5e32188..05cad6a 100644
--- a/internal/lsp/source/signature_help.go
+++ b/internal/lsp/source/signature_help.go
@@ -18,41 +18,31 @@
errors "golang.org/x/xerrors"
)
-type SignatureInformation struct {
- Label, Documentation string
- Parameters []ParameterInformation
- ActiveParameter int
-}
-
-type ParameterInformation struct {
- Label string
-}
-
-func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (*SignatureInformation, error) {
+func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (*protocol.SignatureInformation, int, error) {
ctx, done := trace.StartSpan(ctx, "source.SignatureHelp")
defer done()
pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle)
if err != nil {
- return nil, fmt.Errorf("getting file for SignatureHelp: %v", err)
+ return nil, 0, fmt.Errorf("getting file for SignatureHelp: %v", err)
}
- file, m, _, err := pgh.Cached()
+ file, _, m, _, err := pgh.Cached()
if err != nil {
- return nil, err
+ return nil, 0, err
}
spn, err := m.PointSpan(pos)
if err != nil {
- return nil, err
+ return nil, 0, err
}
rng, err := spn.Range(m.Converter)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Find a call expression surrounding the query position.
var callExpr *ast.CallExpr
path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.Start)
if path == nil {
- return nil, errors.Errorf("cannot find node enclosing position")
+ return nil, 0, errors.Errorf("cannot find node enclosing position")
}
FindCall:
for _, node := range path {
@@ -66,11 +56,11 @@
// The user is within an anonymous function,
// which may be the parameter to the *ast.CallExpr.
// Don't show signature help in this case.
- return nil, errors.Errorf("no signature help within a function declaration")
+ return nil, 0, errors.Errorf("no signature help within a function declaration")
}
}
if callExpr == nil || callExpr.Fun == nil {
- return nil, errors.Errorf("cannot find an enclosing function")
+ return nil, 0, errors.Errorf("cannot find an enclosing function")
}
// Get the object representing the function, if available.
@@ -92,12 +82,12 @@
// Get the type information for the function being called.
sigType := pkg.GetTypesInfo().TypeOf(callExpr.Fun)
if sigType == nil {
- return nil, errors.Errorf("cannot get type for Fun %[1]T (%[1]v)", callExpr.Fun)
+ return nil, 0, errors.Errorf("cannot get type for Fun %[1]T (%[1]v)", callExpr.Fun)
}
sig, _ := sigType.Underlying().(*types.Signature)
if sig == nil {
- return nil, errors.Errorf("cannot find signature for Fun %[1]T (%[1]v)", callExpr.Fun)
+ return nil, 0, errors.Errorf("cannot find signature for Fun %[1]T (%[1]v)", callExpr.Fun)
}
qf := qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo())
@@ -112,11 +102,11 @@
if obj != nil {
node, err := objToNode(snapshot.View(), pkg, obj)
if err != nil {
- return nil, err
+ return nil, 0, err
}
rng, err := objToMappedRange(snapshot.View(), pkg, obj)
if err != nil {
- return nil, err
+ return nil, 0, err
}
decl := &Declaration{
obj: obj,
@@ -125,24 +115,24 @@
}
d, err := decl.hover(ctx)
if err != nil {
- return nil, err
+ return nil, 0, err
}
name = obj.Name()
comment = d.comment
} else {
name = "func"
}
- return signatureInformation(name, comment, params, results, writeResultParens, activeParam), nil
+ return signatureInformation(name, comment, params, results, writeResultParens), activeParam, nil
}
-func builtinSignature(ctx context.Context, v View, callExpr *ast.CallExpr, name string, pos token.Pos) (*SignatureInformation, error) {
+func builtinSignature(ctx context.Context, v View, callExpr *ast.CallExpr, name string, pos token.Pos) (*protocol.SignatureInformation, int, error) {
astObj, err := v.LookupBuiltin(ctx, name)
if err != nil {
- return nil, err
+ return nil, 0, err
}
decl, ok := astObj.Decl.(*ast.FuncDecl)
if !ok {
- return nil, errors.Errorf("no function declaration for builtin: %s", name)
+ return nil, 0, errors.Errorf("no function declaration for builtin: %s", name)
}
params, _ := formatFieldList(ctx, v, decl.Type.Params)
results, writeResultParens := formatFieldList(ctx, v, decl.Type.Results)
@@ -159,24 +149,23 @@
}
}
activeParam := activeParameter(callExpr.Args, numParams, variadic, pos)
- return signatureInformation(name, nil, params, results, writeResultParens, activeParam), nil
+ return signatureInformation(name, nil, params, results, writeResultParens), activeParam, nil
}
-func signatureInformation(name string, comment *ast.CommentGroup, params, results []string, writeResultParens bool, activeParam int) *SignatureInformation {
- paramInfo := make([]ParameterInformation, 0, len(params))
+func signatureInformation(name string, comment *ast.CommentGroup, params, results []string, writeResultParens bool) *protocol.SignatureInformation {
+ paramInfo := make([]protocol.ParameterInformation, 0, len(params))
for _, p := range params {
- paramInfo = append(paramInfo, ParameterInformation{Label: p})
+ paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p})
}
label := name + formatFunction(params, results, writeResultParens)
var c string
if comment != nil {
c = doc.Synopsis(comment.Text())
}
- return &SignatureInformation{
- Label: label,
- Documentation: c,
- Parameters: paramInfo,
- ActiveParameter: activeParam,
+ return &protocol.SignatureInformation{
+ Label: label,
+ Documentation: c,
+ Parameters: paramInfo,
}
}
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 2c766b8..d2721ec 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -5,7 +5,6 @@
package source_test
import (
- "bytes"
"context"
"fmt"
"os"
@@ -45,39 +44,44 @@
func testSource(t *testing.T, exporter packagestest.Exporter) {
ctx := tests.Context(t)
data := tests.Load(t, exporter, "../testdata")
- defer data.Exported.Cleanup()
+ for _, datum := range data {
+ defer datum.Exported.Cleanup()
- cache := cache.New(nil)
- session := cache.NewSession()
- options := tests.DefaultOptions()
- options.Env = data.Config.Env
- view, _, err := session.NewView(ctx, "source_test", span.FileURI(data.Config.Dir), options)
- if err != nil {
- t.Fatal(err)
- }
- r := &runner{
- view: view,
- data: data,
- ctx: ctx,
- }
- var modifications []source.FileModification
- for filename, content := range data.Config.Overlay {
- kind := source.DetectLanguage("", filename)
- if kind != source.Go {
- continue
+ cache := cache.New(nil, nil)
+ session := cache.NewSession()
+ options := tests.DefaultOptions()
+ options.Env = datum.Config.Env
+ view, _, err := session.NewView(ctx, "source_test", span.URIFromPath(datum.Config.Dir), options)
+ if err != nil {
+ t.Fatal(err)
}
- modifications = append(modifications, source.FileModification{
- URI: span.FileURI(filename),
- Action: source.Open,
- Version: -1,
- Text: content,
- LanguageID: "go",
+ r := &runner{
+ view: view,
+ data: datum,
+ ctx: ctx,
+ }
+ var modifications []source.FileModification
+ for filename, content := range datum.Config.Overlay {
+ kind := source.DetectLanguage("", filename)
+ if kind != source.Go {
+ continue
+ }
+ modifications = append(modifications, source.FileModification{
+ URI: span.URIFromPath(filename),
+ Action: source.Open,
+ Version: -1,
+ Text: content,
+ LanguageID: "go",
+ })
+ }
+ if _, err := session.DidModifyFiles(ctx, modifications); err != nil {
+ t.Fatal(err)
+ }
+ t.Run(datum.Folder, func(t *testing.T) {
+ t.Helper()
+ tests.Run(t, r, datum)
})
}
- if _, err := session.DidModifyFiles(ctx, modifications); err != nil {
- t.Fatal(err)
- }
- tests.Run(t, r, data)
}
func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []source.Diagnostic) {
@@ -115,9 +119,7 @@
opts.InsertTextFormat = protocol.SnippetTextFormat
}
})
- if !strings.Contains(string(src.URI()), "builtins") {
- got = tests.FilterBuiltins(got)
- }
+ got = tests.FilterBuiltins(src, got)
if diff := tests.DiffCompletionItems(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
@@ -144,9 +146,7 @@
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
_, got := r.callCompletion(t, src, func(opts *source.Options) {})
- if !strings.Contains(string(src.URI()), "builtins") {
- got = tests.FilterBuiltins(got)
- }
+ got = tests.FilterBuiltins(src, got)
if diff := tests.CheckCompletionOrder(want, got, false); diff != "" {
t.Errorf("%s: %s", src, diff)
}
@@ -162,9 +162,7 @@
opts.Matcher = source.CaseInsensitive
opts.UnimportedCompletion = false
})
- if !strings.Contains(string(src.URI()), "builtins") {
- list = tests.FilterBuiltins(list)
- }
+ list = tests.FilterBuiltins(src, list)
fuzzyMatcher := fuzzy.NewMatcher(prefix)
var got []protocol.CompletionItem
for _, item := range list {
@@ -188,9 +186,7 @@
opts.Matcher = source.Fuzzy
opts.UnimportedCompletion = false
})
- if !strings.Contains(string(src.URI()), "builtins") {
- got = tests.FilterBuiltins(got)
- }
+ got = tests.FilterBuiltins(src, got)
if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
}
@@ -205,9 +201,7 @@
opts.Matcher = source.CaseSensitive
opts.UnimportedCompletion = false
})
- if !strings.Contains(string(src.URI()), "builtins") {
- list = tests.FilterBuiltins(list)
- }
+ list = tests.FilterBuiltins(src, list)
if diff := tests.DiffCompletionItems(want, list); diff != "" {
t.Errorf("%s: %s", src, diff)
}
@@ -484,7 +478,7 @@
if err != nil {
t.Fatal(err)
}
- ident, err := source.Identifier(r.ctx, r.view.Snapshot(), fh, srcRng.Start, source.WidestPackageHandle)
+ ident, err := source.Identifier(r.ctx, r.view.Snapshot(), fh, srcRng.Start)
if err != nil {
t.Fatalf("failed for %v: %v", d.Src, err)
}
@@ -553,7 +547,7 @@
}
var results []span.Span
for i := range locs {
- locURI := span.NewURI(locs[i].URI)
+ locURI := locs[i].URI.SpanURI()
lm, err := r.data.Mapper(locURI)
if err != nil {
t.Fatal(err)
@@ -768,7 +762,7 @@
}
return
}
- if want.Text == "" && item != nil {
+ if want.Text == "" {
t.Errorf("prepare rename failed for %v: expected nil, got %v", src, item)
return
}
@@ -798,55 +792,73 @@
t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols))
return
}
- if diff := r.diffSymbols(t, uri, expectedSymbols, symbols); diff != "" {
+ if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" {
t.Error(diff)
}
}
-func (r *runner) diffSymbols(t *testing.T, uri span.URI, want, got []protocol.DocumentSymbol) string {
- sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name })
- sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name })
- if len(got) != len(want) {
- return summarizeSymbols(t, -1, want, got, "different lengths got %v want %v", len(got), len(want))
+func (r *runner) WorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
+ got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
+ opts.Matcher = source.CaseInsensitive
+ })
+ got = tests.FilterWorkspaceSymbols(got, dirs)
+ if len(got) != len(expectedSymbols) {
+ t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
+ return
}
- for i, w := range want {
- g := got[i]
- if w.Name != g.Name {
- return summarizeSymbols(t, i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
- }
- if w.Kind != g.Kind {
- return summarizeSymbols(t, i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
- }
- if protocol.CompareRange(w.SelectionRange, g.SelectionRange) != 0 {
- return summarizeSymbols(t, i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange)
- }
- if msg := r.diffSymbols(t, uri, w.Children, g.Children); msg != "" {
- return fmt.Sprintf("children of %s: %s", w.Name, msg)
- }
+ if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
+ t.Error(diff)
}
- return ""
}
-func summarizeSymbols(t *testing.T, i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
- msg := &bytes.Buffer{}
- fmt.Fprint(msg, "document symbols failed")
- if i >= 0 {
- fmt.Fprintf(msg, " at %d", i)
+func (r *runner) FuzzyWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
+ got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
+ opts.Matcher = source.Fuzzy
+ })
+ got = tests.FilterWorkspaceSymbols(got, dirs)
+ if len(got) != len(expectedSymbols) {
+ t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
+ return
}
- fmt.Fprint(msg, " because of ")
- fmt.Fprintf(msg, reason, args...)
- fmt.Fprint(msg, ":\nexpected:\n")
- for _, s := range want {
- fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
+ if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
+ t.Error(diff)
}
- fmt.Fprintf(msg, "got:\n")
- for _, s := range got {
- fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
- }
- return msg.String()
}
-func (r *runner) SignatureHelp(t *testing.T, spn span.Span, expectedSignature *source.SignatureInformation) {
+func (r *runner) CaseSensitiveWorkspaceSymbols(t *testing.T, query string, expectedSymbols []protocol.SymbolInformation, dirs map[string]struct{}) {
+ got := r.callWorkspaceSymbols(t, query, func(opts *source.Options) {
+ opts.Matcher = source.CaseSensitive
+ })
+ got = tests.FilterWorkspaceSymbols(got, dirs)
+ if len(got) != len(expectedSymbols) {
+ t.Errorf("want %d symbols, got %d", len(expectedSymbols), len(got))
+ return
+ }
+ if diff := tests.DiffWorkspaceSymbols(expectedSymbols, got); diff != "" {
+ t.Error(diff)
+ }
+}
+
+func (r *runner) callWorkspaceSymbols(t *testing.T, query string, options func(*source.Options)) []protocol.SymbolInformation {
+ t.Helper()
+
+ original := r.view.Options()
+ modified := original
+ options(&modified)
+ view, err := r.view.SetOptions(r.ctx, modified)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.view.SetOptions(r.ctx, original)
+
+ got, err := source.WorkspaceSymbols(r.ctx, []source.View{view}, query)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return got
+}
+
+func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) {
_, rng, err := spanToRange(r.data, spn)
if err != nil {
t.Fatal(err)
@@ -855,46 +867,34 @@
if err != nil {
t.Fatal(err)
}
- gotSignature, err := source.SignatureHelp(r.ctx, r.view.Snapshot(), fh, rng.Start)
+ gotSignature, gotActiveParameter, err := source.SignatureHelp(r.ctx, r.view.Snapshot(), fh, rng.Start)
if err != nil {
// Only fail if we got an error we did not expect.
- if expectedSignature != nil {
+ if want != nil {
t.Fatalf("failed for %v: %v", spn, err)
}
- }
- if expectedSignature == nil {
- if gotSignature != nil {
- t.Errorf("expected no signature, got %v", gotSignature)
- }
return
}
- if diff := diffSignatures(spn, expectedSignature, gotSignature); diff != "" {
+ if gotSignature == nil {
+ if want != nil {
+ t.Fatalf("got nil signature, but expected %v", want)
+ }
+ return
+ }
+ got := &protocol.SignatureHelp{
+ Signatures: []protocol.SignatureInformation{*gotSignature},
+ ActiveParameter: float64(gotActiveParameter),
+ }
+ if diff := tests.DiffSignatures(spn, got, want); diff != "" {
t.Error(diff)
}
}
-func diffSignatures(spn span.Span, want *source.SignatureInformation, got *source.SignatureInformation) string {
- decorate := func(f string, args ...interface{}) string {
- return fmt.Sprintf("Invalid signature at %s: %s", spn, fmt.Sprintf(f, args...))
- }
- if want.ActiveParameter != got.ActiveParameter {
- return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, got.ActiveParameter)
- }
- if want.Label != got.Label {
- return decorate("wanted label %q, got %q", want.Label, got.Label)
- }
- var paramParts []string
- for _, p := range got.Parameters {
- paramParts = append(paramParts, p.Label)
- }
- paramsStr := strings.Join(paramParts, ", ")
- if !strings.Contains(got.Label, paramsStr) {
- return decorate("expected signature %q to contain params %q", got.Label, paramsStr)
- }
- return ""
+func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {
+ // This is a pure LSP feature, no source level functionality to be tested.
}
-func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {
+func (r *runner) CodeLens(t *testing.T, spn span.Span, want []protocol.CodeLens) {
// This is a pure LSP feature, no source level functionality to be tested.
}
diff --git a/internal/lsp/source/symbols.go b/internal/lsp/source/symbols.go
index 989492c..12afcb3 100644
--- a/internal/lsp/source/symbols.go
+++ b/internal/lsp/source/symbols.go
@@ -22,7 +22,7 @@
if err != nil {
return nil, fmt.Errorf("getting file for DocumentSymbols: %v", err)
}
- file, _, _, err := pgh.Cached()
+ file, _, _, _, err := pgh.Cached()
if err != nil {
return nil, err
}
@@ -129,40 +129,12 @@
return s, nil
}
-func setKind(s *protocol.DocumentSymbol, typ types.Type, q types.Qualifier) {
- switch typ := typ.Underlying().(type) {
- case *types.Interface:
- s.Kind = protocol.Interface
- case *types.Struct:
- s.Kind = protocol.Struct
- case *types.Signature:
- s.Kind = protocol.Function
- if typ.Recv() != nil {
- s.Kind = protocol.Method
- }
- case *types.Named:
- setKind(s, typ.Underlying(), q)
- case *types.Basic:
- i := typ.Info()
- switch {
- case i&types.IsNumeric != 0:
- s.Kind = protocol.Number
- case i&types.IsBoolean != 0:
- s.Kind = protocol.Boolean
- case i&types.IsString != 0:
- s.Kind = protocol.String
- }
- default:
- s.Kind = protocol.Variable
- }
-}
-
func typeSymbol(ctx context.Context, view View, pkg Package, info *types.Info, spec *ast.TypeSpec, obj types.Object, q types.Qualifier) (protocol.DocumentSymbol, error) {
s := protocol.DocumentSymbol{
Name: obj.Name(),
}
s.Detail, _ = formatType(obj.Type(), q)
- setKind(&s, obj.Type(), q)
+ s.Kind = typeToKind(obj.Type())
var err error
s.Range, err = nodeToProtocolRange(view, pkg, spec)
@@ -236,7 +208,7 @@
child := protocol.DocumentSymbol{
Name: types.TypeString(embedded, q),
}
- setKind(&child, embedded, q)
+ child.Kind = typeToKind(embedded)
var spanNode, selectionNode ast.Node
Embeddeds:
for _, f := range ai.Methods.List {
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index 34e6317..86b017f 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -147,7 +147,7 @@
return false
}
ph := snapshot.View().Session().Cache().ParseGoHandle(fh, ParseHeader)
- parsed, _, _, err := ph.Parse(ctx)
+ parsed, _, _, _, err := ph.Parse(ctx)
if err != nil {
return false
}
@@ -201,7 +201,7 @@
func posToMappedRange(v View, pkg Package, pos, end token.Pos) (mappedRange, error) {
logicalFilename := v.Session().Cache().FileSet().File(pos).Position(pos).Filename
- m, err := findMapperInPackage(v, pkg, span.FileURI(logicalFilename))
+ m, err := findMapperInPackage(v, pkg, span.URIFromPath(logicalFilename))
if err != nil {
return mappedRange{}, err
}
@@ -415,6 +415,23 @@
return false
}
+func isPkgName(obj types.Object) bool {
+ _, ok := obj.(*types.PkgName)
+ return ok
+}
+
+func isASTFile(n ast.Node) bool {
+ _, ok := n.(*ast.File)
+ return ok
+}
+
+func deslice(T types.Type) types.Type {
+ if slice, ok := T.Underlying().(*types.Slice); ok {
+ return slice.Elem()
+ }
+ return nil
+}
+
// isSelector returns the enclosing *ast.SelectorExpr when pos is in the
// selector.
func enclosingSelector(path []ast.Node, pos token.Pos) *ast.SelectorExpr {
@@ -626,7 +643,7 @@
if tok == nil {
return nil, nil, errors.Errorf("no file for pos in package %s", searchpkg.ID())
}
- uri := span.FileURI(tok.Name())
+ uri := span.URIFromPath(tok.Name())
var (
ph ParseGoHandle
@@ -637,12 +654,12 @@
if v.Ignore(uri) {
ph, err = findIgnoredFile(v, uri)
} else {
- ph, pkg, err = findFileInPackage(searchpkg, uri)
+ ph, pkg, err = FindFileInPackage(searchpkg, uri)
}
if err != nil {
return nil, nil, err
}
- file, _, _, err := ph.Cached()
+ file, _, _, _, err := ph.Cached()
if err != nil {
return nil, nil, err
}
@@ -661,12 +678,12 @@
if v.Ignore(uri) {
ph, err = findIgnoredFile(v, uri)
} else {
- ph, _, err = findFileInPackage(searchpkg, uri)
+ ph, _, err = FindFileInPackage(searchpkg, uri)
}
if err != nil {
return nil, err
}
- _, m, _, err := ph.Cached()
+ _, _, m, _, err := ph.Cached()
if err != nil {
return nil, err
}
@@ -681,7 +698,8 @@
return v.Session().Cache().ParseGoHandle(fh, ParseFull), nil
}
-func findFileInPackage(pkg Package, uri span.URI) (ParseGoHandle, Package, error) {
+// FindFileInPackage finds uri in pkg or its dependencies.
+func FindFileInPackage(pkg Package, uri span.URI) (ParseGoHandle, Package, error) {
queue := []Package{pkg}
seen := make(map[string]bool)
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 053ae79..0c0f5f5 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -16,6 +16,7 @@
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/span"
)
@@ -26,10 +27,19 @@
// View returns the View associated with this snapshot.
View() View
+ // Config returns the configuration for the view.
+ Config(ctx context.Context) *packages.Config
+
// GetFile returns the file object for a given URI, initializing it
// if it is not already part of the view.
GetFile(uri span.URI) (FileHandle, error)
+ // IsOpen returns whether the editor currently has a file open.
+ IsOpen(uri span.URI) bool
+
+ // IsSaved returns whether the contents are saved on disk or not.
+ IsSaved(uri span.URI) bool
+
// Analyze runs the analyses for the given package at this snapshot.
Analyze(ctx context.Context, id string, analyzers []*analysis.Analyzer) ([]*Error, error)
@@ -39,7 +49,11 @@
// ModTidyHandle returns a ModTidyHandle for the given go.mod file handle.
// This function can have no data or error if there is no modfile detected.
- ModTidyHandle(ctx context.Context, fh FileHandle) ModTidyHandle
+ ModTidyHandle(ctx context.Context, fh FileHandle) (ModTidyHandle, error)
+
+ // ModHandle returns a ModHandle for the passed in go.mod file handle.
+ // This function can have no data if there is no modfile detected.
+ ModHandle(ctx context.Context, fh FileHandle) ModHandle
// PackageHandles returns the PackageHandles for the packages that this file
// belongs to.
@@ -54,6 +68,8 @@
CachedImportPaths(ctx context.Context) (map[string]Package, error)
// KnownPackages returns all the packages loaded in this snapshot.
+ // Workspace packages may be parsed in ParseFull mode, whereas transitive
+ // dependencies will be in ParseExported mode.
KnownPackages(ctx context.Context) ([]PackageHandle, error)
// WorkspacePackages returns the PackageHandles for the snapshot's
@@ -109,10 +125,7 @@
// Ignore returns true if this file should be ignored by this view.
Ignore(span.URI) bool
- // Config returns the configuration for the view.
- Config(ctx context.Context) *packages.Config
-
- // RunProcessEnvFunc runs fn with the process env for this view.
+ // RunProcessEnvFunc runs fn with the process env for this snapshot's view.
// Note: the process env contains cached module and filesystem state.
RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error) error
@@ -164,10 +177,6 @@
// content from the underlying cache if no overlay is present.
FileSystem
- // IsOpen returns whether the editor currently has a file open,
- // and if its contents are saved on disk or not.
- IsOpen(uri span.URI) bool
-
// DidModifyFile reports a file modification to the session.
// It returns the resulting snapshots, a guaranteed one per view.
DidModifyFiles(ctx context.Context, changes []FileModification) ([]Snapshot, error)
@@ -220,9 +229,6 @@
// A FileSystem that reads file contents from external storage.
FileSystem
- // NewSession creates a new Session manager and returns it.
- NewSession() Session
-
// FileSet returns the shared fileset used by all files in the system.
FileSet() *token.FileSet
@@ -246,13 +252,34 @@
// Parse returns the parsed AST for the file.
// If the file is not available, returns nil and an error.
- Parse(ctx context.Context) (*ast.File, *protocol.ColumnMapper, error, error)
+ Parse(ctx context.Context) (file *ast.File, src []byte, m *protocol.ColumnMapper, parseErr error, err error)
// Cached returns the AST for this handle, if it has already been stored.
- Cached() (*ast.File, *protocol.ColumnMapper, error, error)
+ Cached() (file *ast.File, src []byte, m *protocol.ColumnMapper, parseErr error, err error)
}
-// ModTidyHandle represents a handle to the modfile for a go.mod.
+// ModHandle represents a handle to the modfile for a go.mod.
+type ModHandle interface {
+ // File returns a file handle for which to get the modfile.
+ File() FileHandle
+
+ // Parse returns the parsed modfile and a mapper for the go.mod file.
+ // If the file is not available, returns nil and an error.
+ Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error)
+
+ // Upgrades returns the parsed modfile, a mapper, and any dependency upgrades
+ // for the go.mod file. Note that this will only work if the go.mod is the view's go.mod.
+ // If the file is not available, returns nil and an error.
+ Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error)
+
+ // Why returns the parsed modfile, a mapper, and any explanations why a dependency should be
+ // in the go.mod file. Note that this will only work if the go.mod is the view's go.mod.
+ // If the file is not available, returns nil and an error.
+ Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error)
+}
+
+// ModTidyHandle represents a handle to the modfile for the view.
+// Specifically for the purpose of getting diagnostics by running "go mod tidy".
type ModTidyHandle interface {
// File returns a file handle for which to get the modfile.
File() FileHandle
@@ -341,8 +368,10 @@
GetTypesInfo() *types.Info
GetTypesSizes() types.Sizes
IsIllTyped() bool
+ ForTest() string
GetImport(pkgPath string) (Package, error)
Imports() []Package
+ Module() *packagesinternal.Module
}
type Error struct {
diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go
new file mode 100644
index 0000000..fdeef1d
--- /dev/null
+++ b/internal/lsp/source/workspace_symbol.go
@@ -0,0 +1,215 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package source
+
+import (
+ "context"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "strings"
+
+ "golang.org/x/tools/internal/lsp/fuzzy"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/telemetry/log"
+ "golang.org/x/tools/internal/telemetry/trace"
+)
+
+const maxSymbols = 100
+
+func WorkspaceSymbols(ctx context.Context, views []View, query string) ([]protocol.SymbolInformation, error) {
+ ctx, done := trace.StartSpan(ctx, "source.WorkspaceSymbols")
+ defer done()
+
+ seen := make(map[string]struct{})
+ var symbols []protocol.SymbolInformation
+outer:
+ for _, view := range views {
+ knownPkgs, err := view.Snapshot().KnownPackages(ctx)
+ if err != nil {
+ return nil, err
+ }
+ matcher := makeMatcher(view.Options().Matcher, query)
+ for _, ph := range knownPkgs {
+ pkg, err := ph.Check(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := seen[pkg.PkgPath()]; ok {
+ continue
+ }
+ seen[pkg.PkgPath()] = struct{}{}
+ for _, fh := range pkg.CompiledGoFiles() {
+ file, _, _, _, err := fh.Cached()
+ if err != nil {
+ return nil, err
+ }
+ for _, si := range findSymbol(file.Decls, pkg.GetTypesInfo(), matcher) {
+ rng, err := nodeToProtocolRange(view, pkg, si.node)
+ if err != nil {
+ log.Error(ctx, "Error getting range for node", err)
+ continue
+ }
+ symbols = append(symbols, protocol.SymbolInformation{
+ Name: si.name,
+ Kind: si.kind,
+ Location: protocol.Location{
+ URI: protocol.URIFromSpanURI(fh.File().Identity().URI),
+ Range: rng,
+ },
+ })
+ if len(symbols) > maxSymbols {
+ break outer
+ }
+ }
+ }
+ }
+ }
+ return symbols, nil
+}
+
+type symbolInformation struct {
+ name string
+ kind protocol.SymbolKind
+ node ast.Node
+}
+
+type matcherFunc func(string) bool
+
+func makeMatcher(m Matcher, query string) matcherFunc {
+ switch m {
+ case Fuzzy:
+ fm := fuzzy.NewMatcher(query)
+ return func(s string) bool {
+ return fm.Score(s) > 0
+ }
+ case CaseSensitive:
+ return func(s string) bool {
+ return strings.Contains(s, query)
+ }
+ default:
+ q := strings.ToLower(query)
+ return func(s string) bool {
+ return strings.Contains(strings.ToLower(s), q)
+ }
+ }
+}
+
+func findSymbol(decls []ast.Decl, info *types.Info, matcher matcherFunc) []symbolInformation {
+ var result []symbolInformation
+ for _, decl := range decls {
+ switch decl := decl.(type) {
+ case *ast.FuncDecl:
+ if matcher(decl.Name.Name) {
+ kind := protocol.Function
+ if decl.Recv != nil {
+ kind = protocol.Method
+ }
+ result = append(result, symbolInformation{
+ name: decl.Name.Name,
+ kind: kind,
+ node: decl.Name,
+ })
+ }
+ case *ast.GenDecl:
+ for _, spec := range decl.Specs {
+ switch spec := spec.(type) {
+ case *ast.TypeSpec:
+ if matcher(spec.Name.Name) {
+ result = append(result, symbolInformation{
+ name: spec.Name.Name,
+ kind: typeToKind(info.TypeOf(spec.Type)),
+ node: spec.Name,
+ })
+ }
+ switch st := spec.Type.(type) {
+ case *ast.StructType:
+ for _, field := range st.Fields.List {
+ result = append(result, findFieldSymbol(field, protocol.Field, matcher)...)
+ }
+ case *ast.InterfaceType:
+ for _, field := range st.Methods.List {
+ kind := protocol.Method
+ if len(field.Names) == 0 {
+ kind = protocol.Interface
+ }
+ result = append(result, findFieldSymbol(field, kind, matcher)...)
+ }
+ }
+ case *ast.ValueSpec:
+ for _, name := range spec.Names {
+ if matcher(name.Name) {
+ kind := protocol.Variable
+ if decl.Tok == token.CONST {
+ kind = protocol.Constant
+ }
+ result = append(result, symbolInformation{
+ name: name.Name,
+ kind: kind,
+ node: name,
+ })
+ }
+ }
+ }
+ }
+ }
+ }
+ return result
+}
+
+func typeToKind(typ types.Type) protocol.SymbolKind {
+ switch typ := typ.Underlying().(type) {
+ case *types.Interface:
+ return protocol.Interface
+ case *types.Struct:
+ return protocol.Struct
+ case *types.Signature:
+ if typ.Recv() != nil {
+ return protocol.Method
+ }
+ return protocol.Function
+ case *types.Named:
+ return typeToKind(typ.Underlying())
+ case *types.Basic:
+ i := typ.Info()
+ switch {
+ case i&types.IsNumeric != 0:
+ return protocol.Number
+ case i&types.IsBoolean != 0:
+ return protocol.Boolean
+ case i&types.IsString != 0:
+ return protocol.String
+ }
+ }
+ return protocol.Variable
+}
+
+func findFieldSymbol(field *ast.Field, kind protocol.SymbolKind, matcher matcherFunc) []symbolInformation {
+ var result []symbolInformation
+
+ if len(field.Names) == 0 {
+ name := types.ExprString(field.Type)
+ if matcher(name) {
+ result = append(result, symbolInformation{
+ name: name,
+ kind: kind,
+ node: field,
+ })
+ }
+ return result
+ }
+
+ for _, name := range field.Names {
+ if matcher(name.Name) {
+ result = append(result, symbolInformation{
+ name: name.Name,
+ kind: kind,
+ node: name,
+ })
+ }
+ }
+
+ return result
+}
diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go
index 0e167d3..b9f0b75 100644
--- a/internal/lsp/symbols.go
+++ b/internal/lsp/symbols.go
@@ -10,7 +10,6 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
- "golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/trace"
)
@@ -19,26 +18,13 @@
ctx, done := trace.StartSpan(ctx, "lsp.Server.documentSymbol")
defer done()
- uri := span.NewURI(params.TextDocument.URI)
- view, err := s.session.ViewOf(uri)
- if err != nil {
- return nil, err
+ snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
+ if !ok {
+ return []protocol.DocumentSymbol{}, err
}
- snapshot := view.Snapshot()
- fh, err := snapshot.GetFile(uri)
+ symbols, err := source.DocumentSymbols(ctx, snapshot, fh)
if err != nil {
- return nil, err
- }
- var symbols []protocol.DocumentSymbol
- switch fh.Identity().Kind {
- case source.Go:
- symbols, err = source.DocumentSymbols(ctx, snapshot, fh)
- case source.Mod:
- return []protocol.DocumentSymbol{}, nil
- }
-
- if err != nil {
- log.Error(ctx, "DocumentSymbols failed", err, telemetry.URI.Of(uri))
+ log.Error(ctx, "DocumentSymbols failed", err, telemetry.URI.Of(fh.Identity().URI))
return []protocol.DocumentSymbol{}, nil
}
return symbols, nil
diff --git a/internal/lsp/testdata/%percent/perc%ent.go b/internal/lsp/testdata/%percent/perc%ent.go
deleted file mode 100644
index f993da8..0000000
--- a/internal/lsp/testdata/%percent/perc%ent.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package percent
-
-import (
-)
-
-func _() {
- var x int //@diag("x", "compiler", "x declared but not used")
-}
\ No newline at end of file
diff --git a/internal/lsp/testdata/bad/bad0.go b/internal/lsp/testdata/bad/bad0.go
deleted file mode 100644
index 5802ac4..0000000
--- a/internal/lsp/testdata/bad/bad0.go
+++ /dev/null
@@ -1,21 +0,0 @@
-// +build go1.11
-
-package bad
-
-func stuff() { //@item(stuff, "stuff", "func()", "func")
- x := "heeeeyyyy"
- random2(x) //@diag("x", "compiler", "cannot use x (variable of type string) as int value in argument to random2")
- random2(1) //@complete("dom", random, random2, random3)
- y := 3 //@diag("y", "compiler", "y declared but not used")
-}
-
-type bob struct { //@item(bob, "bob", "struct{...}", "struct")
- x int
-}
-
-func _() {
- var q int
- _ = &bob{
- f: q, //@diag("f", "compiler", "unknown field f in struct literal")
- }
-}
diff --git a/internal/lsp/testdata/cgoimport/usecgo.go.golden b/internal/lsp/testdata/cgoimport/usecgo.go.golden
deleted file mode 100644
index 14e2077..0000000
--- a/internal/lsp/testdata/cgoimport/usecgo.go.golden
+++ /dev/null
@@ -1,30 +0,0 @@
--- funccgoexample-definition --
-cgo/declarecgo.go:17:6-13: defined here as [`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)
-
-```go
-func cgo.Example()
-```
--- funccgoexample-definition-json --
-{
- "span": {
- "uri": "file://cgo/declarecgo.go",
- "start": {
- "line": 17,
- "column": 6,
- "offset": 153
- },
- "end": {
- "line": 17,
- "column": 13,
- "offset": 160
- }
- },
- "description": "[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)\n\n```go\nfunc cgo.Example()\n```"
-}
-
--- funccgoexample-hover --
-[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)
-
-```go
-func cgo.Example()
-```
diff --git a/internal/lsp/testdata/cgoimport/usegco.go.golden b/internal/lsp/testdata/cgoimport/usegco.go.golden
deleted file mode 100644
index 8d7131b..0000000
--- a/internal/lsp/testdata/cgoimport/usegco.go.golden
+++ /dev/null
@@ -1,2 +0,0 @@
--- funcexample-hover --
-func cgo.Example()
diff --git a/internal/lsp/testdata/circular/double/b/b.go b/internal/lsp/testdata/circular/double/b/b.go
deleted file mode 100644
index b99c92d..0000000
--- a/internal/lsp/testdata/circular/double/b/b.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package b
-
-import (
- _ "golang.org/x/tools/internal/lsp/circular/double/one" //@diag("_ \"golang.org/x/tools/internal/lsp/circular/double/one\"", "compiler", "import cycle not allowed"),diag("\"golang.org/x/tools/internal/lsp/circular/double/one\"", "compiler", "could not import golang.org/x/tools/internal/lsp/circular/double/one (no package for import golang.org/x/tools/internal/lsp/circular/double/one)")
-)
diff --git a/internal/lsp/testdata/circular/self.go b/internal/lsp/testdata/circular/self.go
deleted file mode 100644
index d0cb5b6..0000000
--- a/internal/lsp/testdata/circular/self.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package circular
-
-import (
- _ "golang.org/x/tools/internal/lsp/circular" //@diag("_ \"golang.org/x/tools/internal/lsp/circular\"", "compiler", "import cycle not allowed"),diag("\"golang.org/x/tools/internal/lsp/circular\"", "compiler", "could not import golang.org/x/tools/internal/lsp/circular (no package for import golang.org/x/tools/internal/lsp/circular)")
-)
diff --git a/internal/lsp/testdata/fieldlist/field_list.go b/internal/lsp/testdata/fieldlist/field_list.go
deleted file mode 100644
index c70530a..0000000
--- a/internal/lsp/testdata/fieldlist/field_list.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package fieldlist
-
-var myInt int //@item(flVar, "myInt", "int", "var")
-type myType int //@item(flType, "myType", "int", "type")
-
-func (my) _() {} //@complete(") _", flType, flVar)
-func (my my) _() {} //@complete(" my)"),complete(") _", flType, flVar)
-
-func (myType) _() {} //@complete(") {", flType, flVar)
-
-func (myType) _(my my) {} //@complete(" my)"),complete(") {", flType, flVar)
-
-func (myType) _() my {} //@complete(" {", flType, flVar)
-
-func (myType) _() (my my) {} //@complete(" my"),complete(") {", flType, flVar)
-
-func _() {
- var _ struct {
- //@complete("", flType, flVar)
- m my //@complete(" my"),complete(" //", flType, flVar)
- }
-
- var _ interface {
- //@complete("", flType, flVar)
- m() my //@complete("("),complete(" //", flType, flVar)
- }
-}
diff --git a/internal/lsp/testdata/generated/generated.go b/internal/lsp/testdata/generated/generated.go
deleted file mode 100644
index 27bc69b..0000000
--- a/internal/lsp/testdata/generated/generated.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package generated
-
-// Code generated by generator.go. DO NOT EDIT.
-
-func _() {
- var y int //@diag("y", "compiler", "y declared but not used")
-}
diff --git a/internal/lsp/testdata/generated/generator.go b/internal/lsp/testdata/generated/generator.go
deleted file mode 100644
index 721703a..0000000
--- a/internal/lsp/testdata/generated/generator.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package generated
-
-func _() {
- var x int //@diag("x", "compiler", "x declared but not used")
-}
diff --git a/internal/lsp/testdata/godef/b/b.go.golden b/internal/lsp/testdata/godef/b/b.go.golden
deleted file mode 100644
index 2d4837f..0000000
--- a/internal/lsp/testdata/godef/b/b.go.golden
+++ /dev/null
@@ -1,409 +0,0 @@
--- A-definition --
-godef/a/a.go:16:6-7: defined here as [`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)
-
-```go
-A string //@A
-
-```
--- A-definition-json --
-{
- "span": {
- "uri": "file://godef/a/a.go",
- "start": {
- "line": 16,
- "column": 6,
- "offset": 159
- },
- "end": {
- "line": 16,
- "column": 7,
- "offset": 160
- }
- },
- "description": "[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)\n\n```go\nA string //@A\n\n```"
-}
-
--- A-hover --
-[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)
-
-```go
-A string //@A
-
-```
--- AImport-definition --
-godef/b/b.go:5:2-43: defined here as [`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a)
-
-```go
-package a ("golang.org/x/tools/internal/lsp/godef/a")
-```
--- AImport-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 5,
- "column": 2,
- "offset": 112
- },
- "end": {
- "line": 5,
- "column": 43,
- "offset": 153
- }
- },
- "description": "[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a)\n\n```go\npackage a (\"golang.org/x/tools/internal/lsp/godef/a\")\n```"
-}
-
--- AImport-hover --
-[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a)
-
-```go
-package a ("golang.org/x/tools/internal/lsp/godef/a")
-```
--- AStuff-definition --
-godef/a/a.go:18:6-12: defined here as [`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff)
-
-```go
-func a.AStuff()
-```
--- AStuff-definition-json --
-{
- "span": {
- "uri": "file://godef/a/a.go",
- "start": {
- "line": 18,
- "column": 6,
- "offset": 179
- },
- "end": {
- "line": 18,
- "column": 12,
- "offset": 185
- }
- },
- "description": "[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff)\n\n```go\nfunc a.AStuff()\n```"
-}
-
--- AStuff-hover --
-[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff)
-
-```go
-func a.AStuff()
-```
--- PackageFoo-definition --
-foo/foo.go:1:1-30:16: defined here as myFoo "golang.org/x/tools/internal/lsp/foo" //@mark(myFoo, "myFoo"),godef("foo", PackageFoo),godef("myFoo", myFoo)
--- PackageFoo-definition-json --
-{
- "span": {
- "uri": "file://foo/foo.go",
- "start": {
- "line": 1,
- "column": 1,
- "offset": 0
- },
- "end": {
- "line": 30,
- "column": 16,
- "offset": 922
- }
- },
- "description": "myFoo \"golang.org/x/tools/internal/lsp/foo\" //@mark(myFoo, \"myFoo\"),godef(\"foo\", PackageFoo),godef(\"myFoo\", myFoo)"
-}
-
--- PackageFoo-hover --
-myFoo "golang.org/x/tools/internal/lsp/foo" //@mark(myFoo, "myFoo"),godef("foo", PackageFoo),godef("myFoo", myFoo)
-
--- S1-definition --
-godef/b/b.go:8:6-8: defined here as [`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)
-
-```go
-S1 struct {
- F1 int //@mark(S1F1, "F1")
- S2 //@godef("S2", S2), mark(S1S2, "S2")
- a.A //@godef("A", A)
-}
-```
--- S1-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 8,
- "column": 6,
- "offset": 193
- },
- "end": {
- "line": 8,
- "column": 8,
- "offset": 195
- }
- },
- "description": "[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)\n\n```go\nS1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2), mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", A)\n}\n```"
-}
-
--- S1-hover --
-[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)
-
-```go
-S1 struct {
- F1 int //@mark(S1F1, "F1")
- S2 //@godef("S2", S2), mark(S1S2, "S2")
- a.A //@godef("A", A)
-}
-```
--- S1F1-definition --
-godef/b/b.go:9:2-4: defined here as \@mark\(S1F1, \"F1\"\)
-
-[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)
-
-```go
-field F1 int
-```
--- S1F1-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 9,
- "column": 2,
- "offset": 212
- },
- "end": {
- "line": 9,
- "column": 4,
- "offset": 214
- }
- },
- "description": "\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)\n\n```go\nfield F1 int\n```"
-}
-
--- S1F1-hover --
-\@mark\(S1F1, \"F1\"\)
-
-[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)
-
-```go
-field F1 int
-```
--- S1S2-definition --
-godef/b/b.go:10:2-4: defined here as \@godef\(\"S2\", S2\), mark\(S1S2, \"S2\"\)
-
-[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)
-
-```go
-field S2 S2
-```
--- S1S2-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 10,
- "column": 2,
- "offset": 241
- },
- "end": {
- "line": 10,
- "column": 4,
- "offset": 243
- }
- },
- "description": "\\@godef\\(\\\"S2\\\", S2\\), mark\\(S1S2, \\\"S2\\\"\\)\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)\n\n```go\nfield S2 S2\n```"
-}
-
--- S1S2-hover --
-\@godef\(\"S2\", S2\), mark\(S1S2, \"S2\"\)
-
-[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)
-
-```go
-field S2 S2
-```
--- S2-definition --
-godef/b/b.go:14:6-8: defined here as [`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2)
-
-```go
-S2 struct {
- F1 string //@mark(S2F1, "F1")
- F2 int //@mark(S2F2, "F2")
- *a.A //@godef("A", A),godef("a",AImport)
-}
-```
--- S2-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 14,
- "column": 6,
- "offset": 320
- },
- "end": {
- "line": 14,
- "column": 8,
- "offset": 322
- }
- },
- "description": "[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2)\n\n```go\nS2 struct {\n\tF1 string //@mark(S2F1, \"F1\")\n\tF2 int //@mark(S2F2, \"F2\")\n\t*a.A //@godef(\"A\", A),godef(\"a\",AImport)\n}\n```"
-}
-
--- S2-hover --
-[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2)
-
-```go
-S2 struct {
- F1 string //@mark(S2F1, "F1")
- F2 int //@mark(S2F2, "F2")
- *a.A //@godef("A", A),godef("a",AImport)
-}
-```
--- S2F1-definition --
-godef/b/b.go:15:2-4: defined here as \@mark\(S2F1, \"F1\"\)
-
-[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)
-
-```go
-field F1 string
-```
--- S2F1-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 15,
- "column": 2,
- "offset": 339
- },
- "end": {
- "line": 15,
- "column": 4,
- "offset": 341
- }
- },
- "description": "\\@mark\\(S2F1, \\\"F1\\\"\\)\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)\n\n```go\nfield F1 string\n```"
-}
-
--- S2F1-hover --
-\@mark\(S2F1, \"F1\"\)
-
-[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)
-
-```go
-field F1 string
-```
--- S2F2-definition --
-godef/b/b.go:16:2-4: defined here as \@mark\(S2F2, \"F2\"\)
-
-[`(b.S1).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F2)
-
-```go
-field F2 int
-```
--- S2F2-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 16,
- "column": 2,
- "offset": 372
- },
- "end": {
- "line": 16,
- "column": 4,
- "offset": 374
- }
- },
- "description": "\\@mark\\(S2F2, \\\"F2\\\"\\)\n\n[`(b.S1).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F2)\n\n```go\nfield F2 int\n```"
-}
-
--- S2F2-hover --
-\@mark\(S2F2, \"F2\"\)
-
-[`(b.S1).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F2)
-
-```go
-field F2 int
-```
--- Stuff-definition --
-godef/a/a.go:9:6-11: defined here as func a.Stuff()
--- Stuff-definition-json --
-{
- "span": {
- "uri": "file://godef/a/a.go",
- "start": {
- "line": 9,
- "column": 6,
- "offset": 95
- },
- "end": {
- "line": 9,
- "column": 11,
- "offset": 100
- }
- },
- "description": "func a.Stuff()"
-}
-
--- Stuff-hover --
-func a.Stuff()
--- X-definition --
-godef/b/b.go:37:7-8: defined here as [`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)
-
-```go
-const X untyped int = 0
-```
--- X-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 37,
- "column": 7,
- "offset": 795
- },
- "end": {
- "line": 37,
- "column": 8,
- "offset": 796
- }
- },
- "description": "[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)\n\n```go\nconst X untyped int = 0\n```"
-}
-
--- X-hover --
-[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)
-
-```go
-const X untyped int = 0
-```
--- myFoo-definition --
-godef/b/b.go:4:2-7: defined here as [`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo)
-
-```go
-package myFoo ("golang.org/x/tools/internal/lsp/foo")
-```
--- myFoo-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 4,
- "column": 2,
- "offset": 21
- },
- "end": {
- "line": 4,
- "column": 7,
- "offset": 26
- }
- },
- "description": "[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo)\n\n```go\npackage myFoo (\"golang.org/x/tools/internal/lsp/foo\")\n```"
-}
-
--- myFoo-hover --
-[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo)
-
-```go
-package myFoo ("golang.org/x/tools/internal/lsp/foo")
-```
diff --git a/internal/lsp/testdata/godef/b/c.go.golden b/internal/lsp/testdata/godef/b/c.go.golden
deleted file mode 100644
index cf7e753..0000000
--- a/internal/lsp/testdata/godef/b/c.go.golden
+++ /dev/null
@@ -1,72 +0,0 @@
--- S1-definition --
-godef/b/b.go:8:6-8: defined here as [`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)
-
-```go
-S1 struct {
- F1 int //@mark(S1F1, "F1")
- S2 //@godef("S2", S2), mark(S1S2, "S2")
- a.A //@godef("A", A)
-}
-```
--- S1-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 8,
- "column": 6,
- "offset": 193
- },
- "end": {
- "line": 8,
- "column": 8,
- "offset": 195
- }
- },
- "description": "[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)\n\n```go\nS1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2), mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", A)\n}\n```"
-}
-
--- S1-hover --
-[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)
-
-```go
-S1 struct {
- F1 int //@mark(S1F1, "F1")
- S2 //@godef("S2", S2), mark(S1S2, "S2")
- a.A //@godef("A", A)
-}
-```
--- S1F1-definition --
-godef/b/b.go:9:2-4: defined here as \@mark\(S1F1, \"F1\"\)
-
-[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)
-
-```go
-field F1 int
-```
--- S1F1-definition-json --
-{
- "span": {
- "uri": "file://godef/b/b.go",
- "start": {
- "line": 9,
- "column": 2,
- "offset": 212
- },
- "end": {
- "line": 9,
- "column": 4,
- "offset": 214
- }
- },
- "description": "\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)\n\n```go\nfield F1 int\n```"
-}
-
--- S1F1-hover --
-\@mark\(S1F1, \"F1\"\)
-
-[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)
-
-```go
-field F1 int
-```
diff --git a/internal/lsp/testdata/indirect/modules/example.com/extramodule/pkg/x.go b/internal/lsp/testdata/indirect/modules/example.com/extramodule/pkg/x.go
new file mode 100644
index 0000000..cf7fc67
--- /dev/null
+++ b/internal/lsp/testdata/indirect/modules/example.com/extramodule/pkg/x.go
@@ -0,0 +1,3 @@
+package pkg
+
+const Test = 1
\ No newline at end of file
diff --git a/internal/lsp/testdata/indirect/primarymod/go.mod b/internal/lsp/testdata/indirect/primarymod/go.mod
new file mode 100644
index 0000000..cfc9f72
--- /dev/null
+++ b/internal/lsp/testdata/indirect/primarymod/go.mod
@@ -0,0 +1,5 @@
+module indirect
+
+go 1.12
+//@diag("// indirect", "go mod tidy", "example.com/extramodule should be a direct dependency.", "warning"),suggestedfix("// indirect")
+require example.com/extramodule v1.0.0 // indirect
diff --git a/internal/lsp/testdata/indirect/primarymod/go.mod.golden b/internal/lsp/testdata/indirect/primarymod/go.mod.golden
new file mode 100644
index 0000000..5bc2879
--- /dev/null
+++ b/internal/lsp/testdata/indirect/primarymod/go.mod.golden
@@ -0,0 +1,8 @@
+-- suggestedfix_go.mod_5_40 --
+module indirect
+
+go 1.12
+
+//@diag("// indirect", "go mod tidy", "example.com/extramodule should be a direct dependency.", "warning"),suggestedfix("// indirect")
+require example.com/extramodule v1.0.0
+
diff --git a/internal/lsp/testdata/indirect/primarymod/main.go b/internal/lsp/testdata/indirect/primarymod/main.go
new file mode 100644
index 0000000..2fb1cc8
--- /dev/null
+++ b/internal/lsp/testdata/indirect/primarymod/main.go
@@ -0,0 +1,10 @@
+// Package indirect does something
+package indirect
+
+import (
+ "example.com/extramodule/pkg"
+)
+
+func Yo() {
+ var _ pkg.Test
+}
diff --git a/internal/lsp/testdata/indirect/summary.txt.golden b/internal/lsp/testdata/indirect/summary.txt.golden
new file mode 100644
index 0000000..5c4f74a
--- /dev/null
+++ b/internal/lsp/testdata/indirect/summary.txt.golden
@@ -0,0 +1,28 @@
+-- summary --
+CodeLensCount = 0
+CompletionsCount = 0
+CompletionSnippetCount = 0
+UnimportedCompletionsCount = 0
+DeepCompletionsCount = 0
+FuzzyCompletionsCount = 0
+RankedCompletionsCount = 0
+CaseSensitiveCompletionsCount = 0
+DiagnosticsCount = 1
+FoldingRangesCount = 0
+FormatCount = 0
+ImportCount = 0
+SuggestedFixCount = 1
+DefinitionsCount = 0
+TypeDefinitionsCount = 0
+HighlightsCount = 0
+ReferencesCount = 0
+RenamesCount = 0
+PrepareRenamesCount = 0
+SymbolsCount = 0
+WorkspaceSymbolsCount = 0
+FuzzyWorkspaceSymbolsCount = 0
+CaseSensitiveWorkspaceSymbolsCount = 0
+SignaturesCount = 0
+LinksCount = 0
+ImplementationsCount = 0
+
diff --git a/internal/lsp/testdata/lsp/modules/example.com/extramodule/pkg/x.go b/internal/lsp/testdata/lsp/modules/example.com/extramodule/pkg/x.go
new file mode 100644
index 0000000..cf7fc67
--- /dev/null
+++ b/internal/lsp/testdata/lsp/modules/example.com/extramodule/pkg/x.go
@@ -0,0 +1,3 @@
+package pkg
+
+const Test = 1
\ No newline at end of file
diff --git a/internal/lsp/testdata/lsp/primarymod/%percent/perc%ent.go b/internal/lsp/testdata/lsp/primarymod/%percent/perc%ent.go
new file mode 100644
index 0000000..4fe88d0
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/%percent/perc%ent.go
@@ -0,0 +1,8 @@
+package percent
+
+import (
+)
+
+func _() {
+ var x int //@diag("x", "compiler", "x declared but not used", "error")
+}
\ No newline at end of file
diff --git a/internal/lsp/testdata/address/address.go b/internal/lsp/testdata/lsp/primarymod/address/address.go
similarity index 93%
rename from internal/lsp/testdata/address/address.go
rename to internal/lsp/testdata/lsp/primarymod/address/address.go
index f1c9528..59d5d4c 100644
--- a/internal/lsp/testdata/address/address.go
+++ b/internal/lsp/testdata/lsp/primarymod/address/address.go
@@ -37,6 +37,10 @@
wantsVariadic() //@rank(")", addrCPtr, addrA),snippet(")", addrCPtr, "*c", "*c")
+ var d **int
+ **d //@item(addrDPtr, "**d", "**int", "var")
+ var _ int = _ //@rank("_ //", addrDPtr, addrA),snippet("_ //", addrDPtr, "**d", "**d")
+
type namedPtr *int
var np namedPtr
*np //@item(addrNamedPtr, "*np", "namedPtr", "var")
diff --git a/internal/lsp/testdata/analyzer/bad_test.go b/internal/lsp/testdata/lsp/primarymod/analyzer/bad_test.go
similarity index 66%
rename from internal/lsp/testdata/analyzer/bad_test.go
rename to internal/lsp/testdata/lsp/primarymod/analyzer/bad_test.go
index 9e54056..3c57cd0 100644
--- a/internal/lsp/testdata/analyzer/bad_test.go
+++ b/internal/lsp/testdata/lsp/primarymod/analyzer/bad_test.go
@@ -6,11 +6,11 @@
"testing"
)
-func Testbad(t *testing.T) { //@diag("", "tests", "Testbad has malformed name: first letter after 'Test' must not be lowercase")
+func Testbad(t *testing.T) { //@diag("", "tests", "Testbad has malformed name: first letter after 'Test' must not be lowercase", "warning")
var x sync.Mutex
- _ = x //@diag("x", "copylocks", "assignment copies lock value to _: sync.Mutex")
+ _ = x //@diag("x", "copylocks", "assignment copies lock value to _: sync.Mutex", "warning")
- printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, "printf", "printfWrapper format %s reads arg #1, but call has 0 args")
+ printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, "printf", "printfWrapper format %s reads arg #1, but call has 0 args", "warning")
}
func printfWrapper(format string, args ...interface{}) {
diff --git a/internal/lsp/testdata/anon/anon.go.in b/internal/lsp/testdata/lsp/primarymod/anon/anon.go.in
similarity index 100%
rename from internal/lsp/testdata/anon/anon.go.in
rename to internal/lsp/testdata/lsp/primarymod/anon/anon.go.in
diff --git a/internal/lsp/testdata/append/append.go b/internal/lsp/testdata/lsp/primarymod/append/append.go
similarity index 100%
rename from internal/lsp/testdata/append/append.go
rename to internal/lsp/testdata/lsp/primarymod/append/append.go
diff --git a/internal/lsp/testdata/arraytype/array_type.go.in b/internal/lsp/testdata/lsp/primarymod/arraytype/array_type.go.in
similarity index 71%
rename from internal/lsp/testdata/arraytype/array_type.go.in
rename to internal/lsp/testdata/lsp/primarymod/arraytype/array_type.go.in
index fc05268..a53ee74 100644
--- a/internal/lsp/testdata/arraytype/array_type.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/arraytype/array_type.go.in
@@ -9,14 +9,14 @@
val string //@item(atVal, "val", "string", "var")
)
- [] //@complete(" //", atVal, PackageFoo)
+ [] //@complete(" //", PackageFoo)
- []val //@complete(" //", atVal)
+ []val //@complete(" //")
[]foo.StructFoo //@complete(" //", StructFoo)
[]foo.StructFoo(nil) //@complete("(", StructFoo)
-
+
[]*foo.StructFoo //@complete(" //", StructFoo)
[...]foo.StructFoo //@complete(" //", StructFoo)
@@ -32,12 +32,12 @@
var mark []myInt //@item(atMark, "mark", "[]myInt", "var")
var s []myInt //@item(atS, "s", "[]myInt", "var")
- s = []m //@complete(" //", atMyInt, atMark)
- s = [] //@complete(" //", atMyInt, atMark, atS, PackageFoo)
+ s = []m //@complete(" //", atMyInt)
+ s = [] //@complete(" //", atMyInt, PackageFoo)
var a [1]myInt
- a = [1]m //@complete(" //", atMyInt, atMark)
+ a = [1]m //@complete(" //", atMyInt)
var ds [][]myInt
- ds = [][]m //@complete(" //", atMyInt, atMark)
+ ds = [][]m //@complete(" //", atMyInt)
}
diff --git a/internal/lsp/testdata/assign/assign.go.in b/internal/lsp/testdata/lsp/primarymod/assign/assign.go.in
similarity index 79%
rename from internal/lsp/testdata/assign/assign.go.in
rename to internal/lsp/testdata/lsp/primarymod/assign/assign.go.in
index f07c461..3fe256e 100644
--- a/internal/lsp/testdata/assign/assign.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/assign/assign.go.in
@@ -1,6 +1,9 @@
package assign
+import "golang.org/x/tools/internal/lsp/assign/internal/secret"
+
func _() {
+ secret.Hello()
var (
myInt int //@item(assignInt, "myInt", "int", "var")
myStr string //@item(assignStr, "myStr", "string", "var")
diff --git a/internal/lsp/testdata/lsp/primarymod/assign/internal/secret/secret.go b/internal/lsp/testdata/lsp/primarymod/assign/internal/secret/secret.go
new file mode 100644
index 0000000..5ee1554
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/assign/internal/secret/secret.go
@@ -0,0 +1,3 @@
+package secret
+
+func Hello() {}
\ No newline at end of file
diff --git a/internal/lsp/testdata/lsp/primarymod/bad/bad0.go b/internal/lsp/testdata/lsp/primarymod/bad/bad0.go
new file mode 100644
index 0000000..cfde87c
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/bad/bad0.go
@@ -0,0 +1,23 @@
+// +build go1.11
+
+package bad
+
+import _ "golang.org/x/tools/internal/lsp/assign/internal/secret" //@diag("\"golang.org/x/tools/internal/lsp/assign/internal/secret\"", "compiler", "could not import golang.org/x/tools/internal/lsp/assign/internal/secret (invalid use of internal package golang.org/x/tools/internal/lsp/assign/internal/secret)", "error")
+
+func stuff() { //@item(stuff, "stuff", "func()", "func")
+ x := "heeeeyyyy"
+ random2(x) //@diag("x", "compiler", "cannot use x (variable of type string) as int value in argument to random2", "error")
+ random2(1) //@complete("dom", random, random2, random3)
+ y := 3 //@diag("y", "compiler", "y declared but not used", "error")
+}
+
+type bob struct { //@item(bob, "bob", "struct{...}", "struct")
+ x int
+}
+
+func _() {
+ var q int
+ _ = &bob{
+ f: q, //@diag("f", "compiler", "unknown field f in struct literal", "error")
+ }
+}
diff --git a/internal/lsp/testdata/bad/bad1.go b/internal/lsp/testdata/lsp/primarymod/bad/bad1.go
similarity index 60%
rename from internal/lsp/testdata/bad/bad1.go
rename to internal/lsp/testdata/lsp/primarymod/bad/bad1.go
index f6ad8c2..512f2d9 100644
--- a/internal/lsp/testdata/bad/bad1.go
+++ b/internal/lsp/testdata/lsp/primarymod/bad/bad1.go
@@ -5,7 +5,7 @@
// See #36637
type stateFunc func() stateFunc //@item(stateFunc, "stateFunc", "func() stateFunc", "type")
-var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", "compiler", "undeclared name: unknown")
+var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", "compiler", "undeclared name: unknown", "error")
func random() int { //@item(random, "random", "func() int", "func")
//@complete("", global_a, bob, random, random2, random3, stateFunc, stuff)
@@ -13,9 +13,9 @@
}
func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func"),item(bad_y_param, "y", "int", "var")
- x := 6 //@item(x, "x", "int", "var"),diag("x", "compiler", "x declared but not used")
- var q blah //@item(q, "q", "blah", "var"),diag("q", "compiler", "q declared but not used"),diag("blah", "compiler", "undeclared name: blah")
- var t **blob //@item(t, "t", "**blob", "var"),diag("t", "compiler", "t declared but not used"),diag("blob", "compiler", "undeclared name: blob")
+ x := 6 //@item(x, "x", "int", "var"),diag("x", "compiler", "x declared but not used", "error")
+ var q blah //@item(q, "q", "blah", "var"),diag("q", "compiler", "q declared but not used", "error"),diag("blah", "compiler", "undeclared name: blah", "error")
+ var t **blob //@item(t, "t", "**blob", "var"),diag("t", "compiler", "t declared but not used", "error"),diag("blob", "compiler", "undeclared name: blob", "error")
//@complete("", q, t, x, bad_y_param, global_a, bob, random, random2, random3, stateFunc, stuff)
return y
@@ -24,10 +24,10 @@
func random3(y ...int) { //@item(random3, "random3", "func(y ...int)", "func"),item(y_variadic_param, "y", "[]int", "var")
//@complete("", y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff)
- var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", "compiler", "ch declared but not used"),diag("favType1", "compiler", "undeclared name: favType1")
- var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", "compiler", "m declared but not used"),diag("keyType", "compiler", "undeclared name: keyType")
- var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", "compiler", "arr declared but not used"),diag("favType2", "compiler", "undeclared name: favType2")
- var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", "compiler", "fn1 declared but not used"),diag("badResult", "compiler", "undeclared name: badResult")
- var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", "compiler", "fn2 declared but not used"),diag("badParam", "compiler", "undeclared name: badParam")
+ var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", "compiler", "ch declared but not used", "error"),diag("favType1", "compiler", "undeclared name: favType1", "error")
+ var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", "compiler", "m declared but not used", "error"),diag("keyType", "compiler", "undeclared name: keyType", "error")
+ var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", "compiler", "arr declared but not used", "error"),diag("favType2", "compiler", "undeclared name: favType2", "error")
+ var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", "compiler", "fn1 declared but not used", "error"),diag("badResult", "compiler", "undeclared name: badResult", "error")
+ var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", "compiler", "fn2 declared but not used", "error"),diag("badParam", "compiler", "undeclared name: badParam", "error")
//@complete("", arr, ch, fn1, fn2, m, y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff)
}
diff --git a/internal/lsp/testdata/bad/badimport.go b/internal/lsp/testdata/lsp/primarymod/bad/badimport.go
similarity index 64%
rename from internal/lsp/testdata/bad/badimport.go
rename to internal/lsp/testdata/lsp/primarymod/bad/badimport.go
index f7d8a11..fefc22e 100644
--- a/internal/lsp/testdata/bad/badimport.go
+++ b/internal/lsp/testdata/lsp/primarymod/bad/badimport.go
@@ -1,5 +1,5 @@
package bad
import (
- _ "nosuchpkg" //@diag("_", "compiler", "could not import nosuchpkg (no package for import nosuchpkg)")
+ _ "nosuchpkg" //@diag("_", "compiler", "could not import nosuchpkg (no package for import nosuchpkg)", "error")
)
diff --git a/internal/lsp/testdata/badstmt/badstmt.go.in b/internal/lsp/testdata/lsp/primarymod/badstmt/badstmt.go.in
similarity index 88%
rename from internal/lsp/testdata/badstmt/badstmt.go.in
rename to internal/lsp/testdata/lsp/primarymod/badstmt/badstmt.go.in
index c25b080..35cfa54 100644
--- a/internal/lsp/testdata/badstmt/badstmt.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/badstmt/badstmt.go.in
@@ -5,7 +5,7 @@
)
func _() {
- defer foo.F //@complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement")
+ defer foo.F //@complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement", "error")
y := 1
defer foo.F //@complete(" //", Foo)
}
diff --git a/internal/lsp/testdata/badstmt/badstmt_2.go.in b/internal/lsp/testdata/lsp/primarymod/badstmt/badstmt_2.go.in
similarity index 100%
rename from internal/lsp/testdata/badstmt/badstmt_2.go.in
rename to internal/lsp/testdata/lsp/primarymod/badstmt/badstmt_2.go.in
diff --git a/internal/lsp/testdata/badstmt/badstmt_3.go.in b/internal/lsp/testdata/lsp/primarymod/badstmt/badstmt_3.go.in
similarity index 100%
rename from internal/lsp/testdata/badstmt/badstmt_3.go.in
rename to internal/lsp/testdata/lsp/primarymod/badstmt/badstmt_3.go.in
diff --git a/internal/lsp/testdata/badstmt/badstmt_4.go.in b/internal/lsp/testdata/lsp/primarymod/badstmt/badstmt_4.go.in
similarity index 100%
rename from internal/lsp/testdata/badstmt/badstmt_4.go.in
rename to internal/lsp/testdata/lsp/primarymod/badstmt/badstmt_4.go.in
diff --git a/internal/lsp/testdata/bar/bar.go.in b/internal/lsp/testdata/lsp/primarymod/bar/bar.go.in
similarity index 89%
rename from internal/lsp/testdata/bar/bar.go.in
rename to internal/lsp/testdata/lsp/primarymod/bar/bar.go.in
index dd80911..70b69b8 100644
--- a/internal/lsp/testdata/bar/bar.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/bar/bar.go.in
@@ -10,13 +10,13 @@
func _() {
help //@complete("l", helper)
- _ = foo.StructFoo{} //@complete("S", IntFoo, StructFoo, Foo)
+ _ = foo.StructFoo{} //@complete("S", IntFoo, StructFoo)
}
// Bar is a function.
func Bar() { //@item(Bar, "Bar", "func()", "func", "Bar is a function.")
foo.Foo() //@complete("F", Foo, IntFoo, StructFoo)
- var _ foo.IntFoo //@complete("I", Foo, IntFoo, StructFoo)
+ var _ foo.IntFoo //@complete("I", IntFoo, StructFoo)
foo.() //@complete("(", Foo, IntFoo, StructFoo)
}
diff --git a/internal/lsp/testdata/basiclit/basiclit.go b/internal/lsp/testdata/lsp/primarymod/basiclit/basiclit.go
similarity index 100%
rename from internal/lsp/testdata/basiclit/basiclit.go
rename to internal/lsp/testdata/lsp/primarymod/basiclit/basiclit.go
diff --git a/internal/lsp/testdata/baz/baz.go.in b/internal/lsp/testdata/lsp/primarymod/baz/baz.go.in
similarity index 77%
rename from internal/lsp/testdata/baz/baz.go.in
rename to internal/lsp/testdata/lsp/primarymod/baz/baz.go.in
index 4652e96..3b74ee5 100644
--- a/internal/lsp/testdata/baz/baz.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/baz/baz.go.in
@@ -20,13 +20,13 @@
func _() {
bob := f.StructFoo{Value: 5}
- if x := bob. //@complete(" //", Value)
+ if x := bob. //@complete(" //", Value)
switch true == false {
case true:
- if x := bob. //@complete(" //", Value)
+ if x := bob. //@complete(" //", Value)
case false:
}
- if x := bob.Va //@complete("a", Value)
+ if x := bob.Va //@complete("a", Value)
switch true == true {
default:
}
diff --git a/internal/lsp/testdata/builtins/builtin_args.go b/internal/lsp/testdata/lsp/primarymod/builtins/builtin_args.go
similarity index 71%
rename from internal/lsp/testdata/builtins/builtin_args.go
rename to internal/lsp/testdata/lsp/primarymod/builtins/builtin_args.go
index 040d7e3..4556021 100644
--- a/internal/lsp/testdata/builtins/builtin_args.go
+++ b/internal/lsp/testdata/lsp/primarymod/builtins/builtin_args.go
@@ -12,10 +12,18 @@
aInt int //@item(builtinInt, "aInt", "int", "var")
)
+ type (
+ aSliceType []int //@item(builtinSliceType, "aSliceType", "[]int", "type")
+ aChanType chan int //@item(builtinChanType, "aChanType", "chan int", "type")
+ aMapType map[string]int //@item(builtinMapType, "aMapType", "map[string]int", "type")
+ )
+
close() //@rank(")", builtinChan, builtinSlice)
append() //@rank(")", builtinSlice, builtinChan)
+ var _ []byte = append([]byte(nil), ""...) //@rank(") //")
+
copy() //@rank(")", builtinSlice, builtinChan)
copy(aSlice, aS) //@rank(")", builtinSlice, builtinString)
copy(aS, aSlice) //@rank(",", builtinSlice, builtinString)
@@ -23,16 +31,21 @@
delete() //@rank(")", builtinMap, builtinChan)
delete(aMap, aS) //@rank(")", builtinString, builtinSlice)
+ aMapFunc := func() map[int]int { //@item(builtinMapFunc, "aMapFunc", "func() map[int]int", "var")
+ return nil
+ }
+ delete() //@rank(")", builtinMapFunc, builtinSlice)
+
len() //@rank(")", builtinSlice, builtinInt),rank(")", builtinMap, builtinInt),rank(")", builtinString, builtinInt),rank(")", builtinArray, builtinInt),rank(")", builtinArrayPtr, builtinPtr),rank(")", builtinChan, builtinInt)
cap() //@rank(")", builtinSlice, builtinMap),rank(")", builtinArray, builtinString),rank(")", builtinArrayPtr, builtinPtr),rank(")", builtinChan, builtinInt)
- make() //@rank(")", builtinMap, builtinInt),rank(")", builtinChan, builtinInt),rank(")", builtinSlice, builtinInt)
+ make() //@rank(")", builtinMapType, int),rank(")", builtinChanType, int),rank(")", builtinSliceType, int),rank(")", builtinMapType, int)
+ make(aSliceType, a) //@rank(")", builtinInt, builtinSlice)
- var _ []int = make() //@rank(")", builtinSlice, builtinMap)
+ var _ []int = make() //@rank(")", builtinSliceType, builtinMapType)
type myStruct struct{} //@item(builtinStructType, "myStruct", "struct{...}", "struct")
- new() //@rank(")", builtinStructType, builtinInt)
var _ *myStruct = new() //@rank(")", builtinStructType, int)
for k := range a { //@rank(" {", builtinSlice, builtinInt),rank(" {", builtinString, builtinInt),rank(" {", builtinChan, builtinInt),rank(" {", builtinArray, builtinInt),rank(" {", builtinArrayPtr, builtinInt),rank(" {", builtinMap, builtinInt),
diff --git a/internal/lsp/testdata/builtins/builtins.go b/internal/lsp/testdata/lsp/primarymod/builtins/builtins.go
similarity index 100%
rename from internal/lsp/testdata/builtins/builtins.go
rename to internal/lsp/testdata/lsp/primarymod/builtins/builtins.go
diff --git a/internal/lsp/testdata/builtins/constants.go b/internal/lsp/testdata/lsp/primarymod/builtins/constants.go
similarity index 100%
rename from internal/lsp/testdata/builtins/constants.go
rename to internal/lsp/testdata/lsp/primarymod/builtins/constants.go
diff --git a/internal/lsp/testdata/casesensitive/casesensitive.go b/internal/lsp/testdata/lsp/primarymod/casesensitive/casesensitive.go
similarity index 100%
rename from internal/lsp/testdata/casesensitive/casesensitive.go
rename to internal/lsp/testdata/lsp/primarymod/casesensitive/casesensitive.go
diff --git a/internal/lsp/testdata/cast/cast.go.in b/internal/lsp/testdata/lsp/primarymod/cast/cast.go.in
similarity index 100%
rename from internal/lsp/testdata/cast/cast.go.in
rename to internal/lsp/testdata/lsp/primarymod/cast/cast.go.in
diff --git a/internal/lsp/testdata/cgo/declarecgo.go b/internal/lsp/testdata/lsp/primarymod/cgo/declarecgo.go
similarity index 100%
rename from internal/lsp/testdata/cgo/declarecgo.go
rename to internal/lsp/testdata/lsp/primarymod/cgo/declarecgo.go
diff --git a/internal/lsp/testdata/cgo/declarecgo_nocgo.go b/internal/lsp/testdata/lsp/primarymod/cgo/declarecgo_nocgo.go
similarity index 100%
rename from internal/lsp/testdata/cgo/declarecgo_nocgo.go
rename to internal/lsp/testdata/lsp/primarymod/cgo/declarecgo_nocgo.go
diff --git a/internal/lsp/testdata/lsp/primarymod/cgoimport/usecgo.go.golden b/internal/lsp/testdata/lsp/primarymod/cgoimport/usecgo.go.golden
new file mode 100644
index 0000000..35937f1
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/cgoimport/usecgo.go.golden
@@ -0,0 +1,30 @@
+-- funccgoexample-definition --
+cgo/declarecgo.go:17:6-13: defined here as ```go
+func cgo.Example()
+```
+
+[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)
+-- funccgoexample-definition-json --
+{
+ "span": {
+ "uri": "file://cgo/declarecgo.go",
+ "start": {
+ "line": 17,
+ "column": 6,
+ "offset": 153
+ },
+ "end": {
+ "line": 17,
+ "column": 13,
+ "offset": 160
+ }
+ },
+ "description": "```go\nfunc cgo.Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)"
+}
+
+-- funccgoexample-hover --
+[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)
+
+```go
+func cgo.Example()
+```
diff --git a/internal/lsp/testdata/cgoimport/usecgo.go.in b/internal/lsp/testdata/lsp/primarymod/cgoimport/usecgo.go.in
similarity index 100%
rename from internal/lsp/testdata/cgoimport/usecgo.go.in
rename to internal/lsp/testdata/lsp/primarymod/cgoimport/usecgo.go.in
diff --git a/internal/lsp/testdata/channel/channel.go b/internal/lsp/testdata/lsp/primarymod/channel/channel.go
similarity index 100%
rename from internal/lsp/testdata/channel/channel.go
rename to internal/lsp/testdata/lsp/primarymod/channel/channel.go
diff --git a/internal/lsp/testdata/lsp/primarymod/circular/double/b/b.go b/internal/lsp/testdata/lsp/primarymod/circular/double/b/b.go
new file mode 100644
index 0000000..1372522
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/circular/double/b/b.go
@@ -0,0 +1,5 @@
+package b
+
+import (
+ _ "golang.org/x/tools/internal/lsp/circular/double/one" //@diag("_ \"golang.org/x/tools/internal/lsp/circular/double/one\"", "compiler", "import cycle not allowed", "error"),diag("\"golang.org/x/tools/internal/lsp/circular/double/one\"", "compiler", "could not import golang.org/x/tools/internal/lsp/circular/double/one (no package for import golang.org/x/tools/internal/lsp/circular/double/one)", "error")
+)
diff --git a/internal/lsp/testdata/circular/double/one/one.go b/internal/lsp/testdata/lsp/primarymod/circular/double/one/one.go
similarity index 100%
rename from internal/lsp/testdata/circular/double/one/one.go
rename to internal/lsp/testdata/lsp/primarymod/circular/double/one/one.go
diff --git a/internal/lsp/testdata/lsp/primarymod/circular/self.go b/internal/lsp/testdata/lsp/primarymod/circular/self.go
new file mode 100644
index 0000000..5ec8b41
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/circular/self.go
@@ -0,0 +1,5 @@
+package circular
+
+import (
+ _ "golang.org/x/tools/internal/lsp/circular" //@diag("_ \"golang.org/x/tools/internal/lsp/circular\"", "compiler", "import cycle not allowed", "error"),diag("\"golang.org/x/tools/internal/lsp/circular\"", "compiler", "could not import golang.org/x/tools/internal/lsp/circular (no package for import golang.org/x/tools/internal/lsp/circular)", "error")
+)
diff --git a/internal/lsp/testdata/circular/triple/a/a.go b/internal/lsp/testdata/lsp/primarymod/circular/triple/a/a.go
similarity index 77%
rename from internal/lsp/testdata/circular/triple/a/a.go
rename to internal/lsp/testdata/lsp/primarymod/circular/triple/a/a.go
index 137c277..64b6c74 100644
--- a/internal/lsp/testdata/circular/triple/a/a.go
+++ b/internal/lsp/testdata/lsp/primarymod/circular/triple/a/a.go
@@ -1,5 +1,5 @@
package a
import (
- _ "golang.org/x/tools/internal/lsp/circular/triple/b" //@diag("_ \"golang.org/x/tools/internal/lsp/circular/triple/b\"", "compiler", "import cycle not allowed")
+ _ "golang.org/x/tools/internal/lsp/circular/triple/b" //@diag("_ \"golang.org/x/tools/internal/lsp/circular/triple/b\"", "compiler", "import cycle not allowed", "error")
)
diff --git a/internal/lsp/testdata/circular/triple/b/b.go b/internal/lsp/testdata/lsp/primarymod/circular/triple/b/b.go
similarity index 100%
rename from internal/lsp/testdata/circular/triple/b/b.go
rename to internal/lsp/testdata/lsp/primarymod/circular/triple/b/b.go
diff --git a/internal/lsp/testdata/circular/triple/c/c.go b/internal/lsp/testdata/lsp/primarymod/circular/triple/c/c.go
similarity index 100%
rename from internal/lsp/testdata/circular/triple/c/c.go
rename to internal/lsp/testdata/lsp/primarymod/circular/triple/c/c.go
diff --git a/internal/lsp/testdata/comments/comments.go b/internal/lsp/testdata/lsp/primarymod/comments/comments.go
similarity index 100%
rename from internal/lsp/testdata/comments/comments.go
rename to internal/lsp/testdata/lsp/primarymod/comments/comments.go
diff --git a/internal/lsp/testdata/complit/complit.go.in b/internal/lsp/testdata/lsp/primarymod/complit/complit.go.in
similarity index 100%
rename from internal/lsp/testdata/complit/complit.go.in
rename to internal/lsp/testdata/lsp/primarymod/complit/complit.go.in
diff --git a/internal/lsp/testdata/constant/constant.go b/internal/lsp/testdata/lsp/primarymod/constant/constant.go
similarity index 100%
rename from internal/lsp/testdata/constant/constant.go
rename to internal/lsp/testdata/lsp/primarymod/constant/constant.go
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for.go
new file mode 100644
index 0000000..a16d3bd
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for.go
@@ -0,0 +1,9 @@
+package danglingstmt
+
+func _() {
+ for bar //@rank(" //", danglingBar)
+}
+
+func bar() bool { //@item(danglingBar, "bar", "func() bool", "func")
+ return true
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for_init.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for_init.go
new file mode 100644
index 0000000..e1130bc
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for_init.go
@@ -0,0 +1,9 @@
+package danglingstmt
+
+func _() {
+ for i := bar //@rank(" //", danglingBar2)
+}
+
+func bar2() int { //@item(danglingBar2, "bar2", "func() int", "func")
+ return 0
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for_init_cond.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for_init_cond.go
new file mode 100644
index 0000000..fb0269f
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for_init_cond.go
@@ -0,0 +1,9 @@
+package danglingstmt
+
+func _() {
+ for i := bar3(); i > bar //@rank(" //", danglingBar3)
+}
+
+func bar3() int { //@item(danglingBar3, "bar3", "func() int", "func")
+ return 0
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for_init_cond_post.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for_init_cond_post.go
new file mode 100644
index 0000000..14f78d3
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_for_init_cond_post.go
@@ -0,0 +1,9 @@
+package danglingstmt
+
+func _() {
+ for i := bar4(); i > bar4(); i += bar //@rank(" //", danglingBar4)
+}
+
+func bar4() int { //@item(danglingBar4, "bar4", "func() int", "func")
+ return 0
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if.go
new file mode 100644
index 0000000..91f145a
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if.go
@@ -0,0 +1,9 @@
+package danglingstmt
+
+func _() {
+ if foo //@rank(" //", danglingFoo)
+}
+
+func foo() bool { //@item(danglingFoo, "foo", "func() bool", "func")
+ return true
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if_eof.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if_eof.go
new file mode 100644
index 0000000..3454c9f
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if_eof.go
@@ -0,0 +1,8 @@
+package danglingstmt
+
+func bar5() bool { //@item(danglingBar5, "bar5", "func() bool", "func")
+ return true
+}
+
+func _() {
+ if b //@rank(" //", danglingBar5)
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if_init.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if_init.go
new file mode 100644
index 0000000..887c318
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if_init.go
@@ -0,0 +1,9 @@
+package danglingstmt
+
+func _() {
+ if i := foo //@rank(" //", danglingFoo2)
+}
+
+func foo2() bool { //@item(danglingFoo2, "foo2", "func() bool", "func")
+ return true
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if_init_cond.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if_init_cond.go
new file mode 100644
index 0000000..5371283
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_if_init_cond.go
@@ -0,0 +1,9 @@
+package danglingstmt
+
+func _() {
+ if i := 123; foo //@rank(" //", danglingFoo3)
+}
+
+func foo3() bool { //@item(danglingFoo3, "foo3", "func() bool", "func")
+ return true
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_multiline_if.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_multiline_if.go
new file mode 100644
index 0000000..2213777
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_multiline_if.go
@@ -0,0 +1,10 @@
+package danglingstmt
+
+func walrus() bool { //@item(danglingWalrus, "walrus", "func() bool", "func")
+ return true
+}
+
+func _() {
+ if true &&
+ walrus //@complete(" //", danglingWalrus)
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_selector_1.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_selector_1.go
new file mode 100644
index 0000000..772152f
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_selector_1.go
@@ -0,0 +1,7 @@
+package danglingstmt
+
+func _() {
+ x. //@rank(" //", danglingI)
+}
+
+var x struct { i int } //@item(danglingI, "i", "int", "field")
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_selector_2.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_selector_2.go
new file mode 100644
index 0000000..a9e75e8
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_selector_2.go
@@ -0,0 +1,8 @@
+package danglingstmt
+
+import "golang.org/x/tools/internal/lsp/foo"
+
+func _() {
+ foo. //@rank(" //", Foo)
+ var _ = []string{foo.} //@rank("}", Foo)
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_switch_init.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_switch_init.go
new file mode 100644
index 0000000..15da3ce
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_switch_init.go
@@ -0,0 +1,9 @@
+package danglingstmt
+
+func _() {
+ switch i := baz //@rank(" //", danglingBaz)
+}
+
+func baz() int { //@item(danglingBaz, "baz", "func() int", "func")
+ return 0
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_switch_init_tag.go b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_switch_init_tag.go
new file mode 100644
index 0000000..20b825b
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/danglingstmt/dangling_switch_init_tag.go
@@ -0,0 +1,9 @@
+package danglingstmt
+
+func _() {
+ switch i := 0; baz //@rank(" //", danglingBaz2)
+}
+
+func baz2() int { //@item(danglingBaz2, "baz2", "func() int", "func")
+ return 0
+}
diff --git a/internal/lsp/testdata/deep/deep.go b/internal/lsp/testdata/lsp/primarymod/deep/deep.go
similarity index 100%
rename from internal/lsp/testdata/deep/deep.go
rename to internal/lsp/testdata/lsp/primarymod/deep/deep.go
diff --git a/internal/lsp/testdata/errors/errors.go b/internal/lsp/testdata/lsp/primarymod/errors/errors.go
similarity index 100%
rename from internal/lsp/testdata/errors/errors.go
rename to internal/lsp/testdata/lsp/primarymod/errors/errors.go
diff --git a/internal/lsp/testdata/lsp/primarymod/fieldlist/field_list.go b/internal/lsp/testdata/lsp/primarymod/fieldlist/field_list.go
new file mode 100644
index 0000000..e687def
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/fieldlist/field_list.go
@@ -0,0 +1,27 @@
+package fieldlist
+
+var myInt int //@item(flVar, "myInt", "int", "var")
+type myType int //@item(flType, "myType", "int", "type")
+
+func (my) _() {} //@complete(") _", flType)
+func (my my) _() {} //@complete(" my)"),complete(") _", flType)
+
+func (myType) _() {} //@complete(") {", flType)
+
+func (myType) _(my my) {} //@complete(" my)"),complete(") {", flType)
+
+func (myType) _() my {} //@complete(" {", flType)
+
+func (myType) _() (my my) {} //@complete(" my"),complete(") {", flType)
+
+func _() {
+ var _ struct {
+ //@complete("", flType)
+ m my //@complete(" my"),complete(" //", flType)
+ }
+
+ var _ interface {
+ //@complete("", flType)
+ m() my //@complete("("),complete(" //", flType)
+ }
+}
diff --git a/internal/lsp/testdata/folding/a.go b/internal/lsp/testdata/lsp/primarymod/folding/a.go
similarity index 100%
rename from internal/lsp/testdata/folding/a.go
rename to internal/lsp/testdata/lsp/primarymod/folding/a.go
diff --git a/internal/lsp/testdata/folding/a.go.golden b/internal/lsp/testdata/lsp/primarymod/folding/a.go.golden
similarity index 100%
rename from internal/lsp/testdata/folding/a.go.golden
rename to internal/lsp/testdata/lsp/primarymod/folding/a.go.golden
diff --git a/internal/lsp/testdata/folding/bad.go.golden b/internal/lsp/testdata/lsp/primarymod/folding/bad.go.golden
similarity index 100%
rename from internal/lsp/testdata/folding/bad.go.golden
rename to internal/lsp/testdata/lsp/primarymod/folding/bad.go.golden
diff --git a/internal/lsp/testdata/folding/bad.go.in b/internal/lsp/testdata/lsp/primarymod/folding/bad.go.in
similarity index 100%
rename from internal/lsp/testdata/folding/bad.go.in
rename to internal/lsp/testdata/lsp/primarymod/folding/bad.go.in
diff --git a/internal/lsp/testdata/foo/foo.go b/internal/lsp/testdata/lsp/primarymod/foo/foo.go
similarity index 90%
rename from internal/lsp/testdata/foo/foo.go
rename to internal/lsp/testdata/lsp/primarymod/foo/foo.go
index 277ac53..20ea183 100644
--- a/internal/lsp/testdata/foo/foo.go
+++ b/internal/lsp/testdata/lsp/primarymod/foo/foo.go
@@ -27,4 +27,4 @@
}
}
-type IntFoo int //@item(IntFoo, "IntFoo", "int", "type"),complete("", Foo, IntFoo, StructFoo)
+type IntFoo int //@item(IntFoo, "IntFoo", "int", "type")
diff --git a/internal/lsp/testdata/format/bad_format.go.golden b/internal/lsp/testdata/lsp/primarymod/format/bad_format.go.golden
similarity index 70%
rename from internal/lsp/testdata/format/bad_format.go.golden
rename to internal/lsp/testdata/lsp/primarymod/format/bad_format.go.golden
index d3a4059..c2ac5a1 100644
--- a/internal/lsp/testdata/format/bad_format.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/format/bad_format.go.golden
@@ -9,7 +9,7 @@
func hello() {
- var x int //@diag("x", "compiler", "x declared but not used")
+ var x int //@diag("x", "compiler", "x declared but not used", "error")
}
func hi() {
diff --git a/internal/lsp/testdata/format/bad_format.go.in b/internal/lsp/testdata/lsp/primarymod/format/bad_format.go.in
similarity index 69%
rename from internal/lsp/testdata/format/bad_format.go.in
rename to internal/lsp/testdata/lsp/primarymod/format/bad_format.go.in
index a2da140..0618723 100644
--- a/internal/lsp/testdata/format/bad_format.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/format/bad_format.go.in
@@ -11,7 +11,7 @@
- var x int //@diag("x", "compiler", "x declared but not used")
+ var x int //@diag("x", "compiler", "x declared but not used", "error")
}
func hi() {
diff --git a/internal/lsp/testdata/format/good_format.go b/internal/lsp/testdata/lsp/primarymod/format/good_format.go
similarity index 100%
rename from internal/lsp/testdata/format/good_format.go
rename to internal/lsp/testdata/lsp/primarymod/format/good_format.go
diff --git a/internal/lsp/testdata/format/good_format.go.golden b/internal/lsp/testdata/lsp/primarymod/format/good_format.go.golden
similarity index 100%
rename from internal/lsp/testdata/format/good_format.go.golden
rename to internal/lsp/testdata/lsp/primarymod/format/good_format.go.golden
diff --git a/internal/lsp/testdata/format/newline_format.go.golden b/internal/lsp/testdata/lsp/primarymod/format/newline_format.go.golden
similarity index 100%
rename from internal/lsp/testdata/format/newline_format.go.golden
rename to internal/lsp/testdata/lsp/primarymod/format/newline_format.go.golden
diff --git a/internal/lsp/testdata/format/newline_format.go.in b/internal/lsp/testdata/lsp/primarymod/format/newline_format.go.in
similarity index 100%
rename from internal/lsp/testdata/format/newline_format.go.in
rename to internal/lsp/testdata/lsp/primarymod/format/newline_format.go.in
diff --git a/internal/lsp/testdata/format/one_line.go.golden b/internal/lsp/testdata/lsp/primarymod/format/one_line.go.golden
similarity index 100%
rename from internal/lsp/testdata/format/one_line.go.golden
rename to internal/lsp/testdata/lsp/primarymod/format/one_line.go.golden
diff --git a/internal/lsp/testdata/format/one_line.go.in b/internal/lsp/testdata/lsp/primarymod/format/one_line.go.in
similarity index 100%
rename from internal/lsp/testdata/format/one_line.go.in
rename to internal/lsp/testdata/lsp/primarymod/format/one_line.go.in
diff --git a/internal/lsp/testdata/func_rank/func_rank.go.in b/internal/lsp/testdata/lsp/primarymod/func_rank/func_rank.go.in
similarity index 85%
rename from internal/lsp/testdata/func_rank/func_rank.go.in
rename to internal/lsp/testdata/lsp/primarymod/func_rank/func_rank.go.in
index 61ad6e9..f9cc6a1 100644
--- a/internal/lsp/testdata/func_rank/func_rank.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/func_rank/func_rank.go.in
@@ -4,11 +4,11 @@
func stringBFunc() string { return "str" } //@item(stringBFunc, "stringBFunc", "func() string", "func")
type stringer struct{} //@item(stringer, "stringer", "struct{...}", "struct")
-func _() stringer //@complete("tr", stringer, stringAVar, stringBFunc)
+func _() stringer //@complete("tr", stringer)
-func _(val stringer) {} //@complete("tr", stringer, stringAVar, stringBFunc)
+func _(val stringer) {} //@complete("tr", stringer)
-func (stringer) _() {} //@complete("tr", stringer, stringAVar, stringBFunc)
+func (stringer) _() {} //@complete("tr", stringer)
func _() {
var s struct {
diff --git a/internal/lsp/testdata/funcsig/func_sig.go b/internal/lsp/testdata/lsp/primarymod/funcsig/func_sig.go
similarity index 100%
rename from internal/lsp/testdata/funcsig/func_sig.go
rename to internal/lsp/testdata/lsp/primarymod/funcsig/func_sig.go
diff --git a/internal/lsp/testdata/funcvalue/func_value.go b/internal/lsp/testdata/lsp/primarymod/funcvalue/func_value.go
similarity index 100%
rename from internal/lsp/testdata/funcvalue/func_value.go
rename to internal/lsp/testdata/lsp/primarymod/funcvalue/func_value.go
diff --git a/internal/lsp/testdata/fuzzymatch/fuzzymatch.go b/internal/lsp/testdata/lsp/primarymod/fuzzymatch/fuzzymatch.go
similarity index 100%
rename from internal/lsp/testdata/fuzzymatch/fuzzymatch.go
rename to internal/lsp/testdata/lsp/primarymod/fuzzymatch/fuzzymatch.go
diff --git a/internal/lsp/testdata/lsp/primarymod/generated/generated.go b/internal/lsp/testdata/lsp/primarymod/generated/generated.go
new file mode 100644
index 0000000..c92bd9e
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/generated/generated.go
@@ -0,0 +1,7 @@
+package generated
+
+// Code generated by generator.go. DO NOT EDIT.
+
+func _() {
+ var y int //@diag("y", "compiler", "y declared but not used", "error")
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/generated/generator.go b/internal/lsp/testdata/lsp/primarymod/generated/generator.go
new file mode 100644
index 0000000..f26e33c
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/generated/generator.go
@@ -0,0 +1,5 @@
+package generated
+
+func _() {
+ var x int //@diag("x", "compiler", "x declared but not used", "error")
+}
diff --git a/internal/lsp/testdata/godef/a/a.go b/internal/lsp/testdata/lsp/primarymod/godef/a/a.go
similarity index 93%
rename from internal/lsp/testdata/godef/a/a.go
rename to internal/lsp/testdata/lsp/primarymod/godef/a/a.go
index 39eb397..d7df63d 100644
--- a/internal/lsp/testdata/godef/a/a.go
+++ b/internal/lsp/testdata/lsp/primarymod/godef/a/a.go
@@ -13,7 +13,7 @@
x string //@x,hover("x", x)
)
-type A string //@A
+type A string //@mark(AString, "A")
func AStuff() { //@AStuff
x := 5
diff --git a/internal/lsp/testdata/godef/a/a.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/a/a.go.golden
similarity index 74%
rename from internal/lsp/testdata/godef/a/a.go.golden
rename to internal/lsp/testdata/lsp/primarymod/godef/a/a.go.golden
index 70d0c42..f8a398a 100644
--- a/internal/lsp/testdata/godef/a/a.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/godef/a/a.go.golden
@@ -15,11 +15,11 @@
func (*types.object).Name() string
```
-- Random-definition --
-godef/a/random.go:3:6-12: defined here as [`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random)
-
-```go
+godef/a/random.go:3:6-12: defined here as ```go
func Random() int
```
+
+[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random)
-- Random-definition-json --
{
"span": {
@@ -35,7 +35,7 @@
"offset": 22
}
},
- "description": "[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random)\n\n```go\nfunc Random() int\n```"
+ "description": "```go\nfunc Random() int\n```\n\n[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random)"
}
-- Random-hover --
@@ -45,11 +45,11 @@
func Random() int
```
-- Random2-definition --
-godef/a/random.go:8:6-13: defined here as [`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2)
-
-```go
+godef/a/random.go:8:6-13: defined here as ```go
func Random2(y int) int
```
+
+[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2)
-- Random2-definition-json --
{
"span": {
@@ -65,7 +65,7 @@
"offset": 78
}
},
- "description": "[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2)\n\n```go\nfunc Random2(y int) int\n```"
+ "description": "```go\nfunc Random2(y int) int\n```\n\n[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2)"
}
-- Random2-hover --
@@ -85,12 +85,12 @@
"start": {
"line": 23,
"column": 6,
- "offset": 287
+ "offset": 304
},
"end": {
"line": 23,
"column": 9,
- "offset": 290
+ "offset": 307
}
},
"description": "```go\nvar err error\n```"
diff --git a/internal/lsp/testdata/lsp/primarymod/godef/a/a_test.go b/internal/lsp/testdata/lsp/primarymod/godef/a/a_test.go
new file mode 100644
index 0000000..77bd633
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/godef/a/a_test.go
@@ -0,0 +1,8 @@
+package a
+
+import (
+ "testing"
+)
+
+func TestA(t *testing.T) { //@TestA,godef(TestA, TestA)
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/godef/a/a_test.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/a/a_test.go.golden
new file mode 100644
index 0000000..ac50b90
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/godef/a/a_test.go.golden
@@ -0,0 +1,26 @@
+-- TestA-definition --
+godef/a/a_test.go:7:6-11: defined here as ```go
+func TestA(t *testing.T)
+```
+-- TestA-definition-json --
+{
+ "span": {
+ "uri": "file://godef/a/a_test.go",
+ "start": {
+ "line": 7,
+ "column": 6,
+ "offset": 39
+ },
+ "end": {
+ "line": 7,
+ "column": 11,
+ "offset": 44
+ }
+ },
+ "description": "```go\nfunc TestA(t *testing.T)\n```"
+}
+
+-- TestA-hover --
+```go
+func TestA(t *testing.T)
+```
diff --git a/internal/lsp/testdata/lsp/primarymod/godef/a/a_x_test.go b/internal/lsp/testdata/lsp/primarymod/godef/a/a_x_test.go
new file mode 100644
index 0000000..85f21cc
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/godef/a/a_x_test.go
@@ -0,0 +1,8 @@
+package a_test
+
+import (
+ "testing"
+)
+
+func TestA2(t *testing.T) { //@TestA2,godef(TestA2, TestA2)
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/godef/a/a_x_test.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/a/a_x_test.go.golden
new file mode 100644
index 0000000..dd1d740
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/godef/a/a_x_test.go.golden
@@ -0,0 +1,26 @@
+-- TestA2-definition --
+godef/a/a_x_test.go:7:6-12: defined here as ```go
+func TestA2(t *testing.T)
+```
+-- TestA2-definition-json --
+{
+ "span": {
+ "uri": "file://godef/a/a_x_test.go",
+ "start": {
+ "line": 7,
+ "column": 6,
+ "offset": 44
+ },
+ "end": {
+ "line": 7,
+ "column": 12,
+ "offset": 50
+ }
+ },
+ "description": "```go\nfunc TestA2(t *testing.T)\n```"
+}
+
+-- TestA2-hover --
+```go
+func TestA2(t *testing.T)
+```
diff --git a/internal/lsp/testdata/godef/a/d.go b/internal/lsp/testdata/lsp/primarymod/godef/a/d.go
similarity index 100%
rename from internal/lsp/testdata/godef/a/d.go
rename to internal/lsp/testdata/lsp/primarymod/godef/a/d.go
diff --git a/internal/lsp/testdata/godef/a/d.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/a/d.go.golden
similarity index 62%
rename from internal/lsp/testdata/godef/a/d.go.golden
rename to internal/lsp/testdata/lsp/primarymod/godef/a/d.go.golden
index 7d2d244..771f98a 100644
--- a/internal/lsp/testdata/godef/a/d.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/godef/a/d.go.golden
@@ -1,11 +1,11 @@
-- Member-definition --
-godef/a/d.go:6:2-8: defined here as \@Member
+godef/a/d.go:6:2-8: defined here as ```go
+field Member string
+```
[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)
-```go
-field Member string
-```
+\@Member
-- Member-definition-json --
{
"span": {
@@ -21,7 +21,7 @@
"offset": 61
}
},
- "description": "\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)\n\n```go\nfield Member string\n```"
+ "description": "```go\nfield Member string\n```\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)\n\n\\@Member"
}
-- Member-hover --
@@ -33,11 +33,11 @@
field Member string
```
-- Method-definition --
-godef/a/d.go:15:16-22: defined here as [`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method)
-
-```go
+godef/a/d.go:15:16-22: defined here as ```go
func (Thing).Method(i int) string
```
+
+[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method)
-- Method-definition-json --
{
"span": {
@@ -53,7 +53,7 @@
"offset": 190
}
},
- "description": "[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method)\n\n```go\nfunc (Thing).Method(i int) string\n```"
+ "description": "```go\nfunc (Thing).Method(i int) string\n```\n\n[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method)"
}
-- Method-hover --
@@ -63,11 +63,11 @@
func (Thing).Method(i int) string
```
-- Other-definition --
-godef/a/d.go:9:5-10: defined here as [`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)
-
-```go
+godef/a/d.go:9:5-10: defined here as ```go
var Other Thing
```
+
+[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)
-- Other-definition-json --
{
"span": {
@@ -83,7 +83,7 @@
"offset": 91
}
},
- "description": "[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\n\n```go\nvar Other Thing\n```"
+ "description": "```go\nvar Other Thing\n```\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"
}
-- Other-hover --
@@ -93,13 +93,13 @@
var Other Thing
```
-- Thing-definition --
-godef/a/d.go:5:6-11: defined here as [`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)
-
-```go
+godef/a/d.go:5:6-11: defined here as ```go
Thing struct {
Member string //@Member
}
```
+
+[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)
-- Thing-definition-json --
{
"span": {
@@ -115,7 +115,7 @@
"offset": 35
}
},
- "description": "[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)\n\n```go\nThing struct {\n\tMember string //@Member\n}\n```"
+ "description": "```go\nThing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)"
}
-- Thing-hover --
@@ -127,11 +127,11 @@
}
```
-- Things-definition --
-godef/a/d.go:11:6-12: defined here as [`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)
-
-```go
+godef/a/d.go:11:6-12: defined here as ```go
func Things(val []string) []Thing
```
+
+[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)
-- Things-definition-json --
{
"span": {
@@ -147,7 +147,7 @@
"offset": 119
}
},
- "description": "[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)\n\n```go\nfunc Things(val []string) []Thing\n```"
+ "description": "```go\nfunc Things(val []string) []Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)"
}
-- Things-hover --
diff --git a/internal/lsp/testdata/godef/a/f.go b/internal/lsp/testdata/lsp/primarymod/godef/a/f.go
similarity index 100%
rename from internal/lsp/testdata/godef/a/f.go
rename to internal/lsp/testdata/lsp/primarymod/godef/a/f.go
diff --git a/internal/lsp/testdata/godef/a/f.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/a/f.go.golden
similarity index 100%
rename from internal/lsp/testdata/godef/a/f.go.golden
rename to internal/lsp/testdata/lsp/primarymod/godef/a/f.go.golden
diff --git a/internal/lsp/testdata/godef/a/random.go b/internal/lsp/testdata/lsp/primarymod/godef/a/random.go
similarity index 100%
rename from internal/lsp/testdata/godef/a/random.go
rename to internal/lsp/testdata/lsp/primarymod/godef/a/random.go
diff --git a/internal/lsp/testdata/godef/a/random.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/a/random.go.golden
similarity index 70%
rename from internal/lsp/testdata/godef/a/random.go.golden
rename to internal/lsp/testdata/lsp/primarymod/godef/a/random.go.golden
index 32d7403..ef10f61 100644
--- a/internal/lsp/testdata/godef/a/random.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/godef/a/random.go.golden
@@ -1,9 +1,9 @@
-- PosSum-definition --
-godef/a/random.go:16:15-18: defined here as [`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum)
-
-```go
+godef/a/random.go:16:15-18: defined here as ```go
func (*Pos).Sum() int
```
+
+[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum)
-- PosSum-definition-json --
{
"span": {
@@ -19,7 +19,7 @@
"offset": 251
}
},
- "description": "[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum)\n\n```go\nfunc (*Pos).Sum() int\n```"
+ "description": "```go\nfunc (*Pos).Sum() int\n```\n\n[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum)"
}
-- PosSum-hover --
@@ -29,11 +29,11 @@
func (*Pos).Sum() int
```
-- PosX-definition --
-godef/a/random.go:13:2-3: defined here as \@mark\(PosX, \"x\"\),mark\(PosY, \"y\"\)
-
-```go
+godef/a/random.go:13:2-3: defined here as ```go
field x int
```
+
+\@mark\(PosX, \"x\"\),mark\(PosY, \"y\"\)
-- PosX-definition-json --
{
"span": {
@@ -49,7 +49,7 @@
"offset": 188
}
},
- "description": "\\@mark\\(PosX, \\\"x\\\"\\),mark\\(PosY, \\\"y\\\"\\)\n\n```go\nfield x int\n```"
+ "description": "```go\nfield x int\n```\n\n\\@mark\\(PosX, \\\"x\\\"\\),mark\\(PosY, \\\"y\\\"\\)"
}
-- PosX-hover --
diff --git a/internal/lsp/testdata/godef/b/b.go b/internal/lsp/testdata/lsp/primarymod/godef/b/b.go
similarity index 81%
rename from internal/lsp/testdata/godef/b/b.go
rename to internal/lsp/testdata/lsp/primarymod/godef/b/b.go
index ee3d0f0..ca6c957 100644
--- a/internal/lsp/testdata/godef/b/b.go
+++ b/internal/lsp/testdata/lsp/primarymod/godef/b/b.go
@@ -8,18 +8,18 @@
type S1 struct { //@S1
F1 int //@mark(S1F1, "F1")
S2 //@godef("S2", S2), mark(S1S2, "S2")
- a.A //@godef("A", A)
+ a.A //@godef("A", AString)
}
type S2 struct { //@S2
F1 string //@mark(S2F1, "F1")
F2 int //@mark(S2F2, "F2")
- *a.A //@godef("A", A),godef("a",AImport)
+ *a.A //@godef("A", AString),godef("a",AImport)
}
type S3 struct {
F1 struct {
- a.A //@godef("A", A)
+ a.A //@godef("A", AString)
}
}
@@ -34,4 +34,4 @@
var _ *myFoo.StructFoo //@godef("myFoo", myFoo)
}
-const X = 0 //@mark(X, "X"),godef("X", X)
+const X = 0 //@mark(bX, "X"),godef("X", bX)
diff --git a/internal/lsp/testdata/lsp/primarymod/godef/b/b.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/b/b.go.golden
new file mode 100644
index 0000000..8d00791
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/godef/b/b.go.golden
@@ -0,0 +1,364 @@
+-- AImport-definition --
+godef/b/b.go:5:2-43: defined here as ```go
+package a ("golang.org/x/tools/internal/lsp/godef/a")
+```
+
+[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a)
+-- AImport-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 5,
+ "column": 2,
+ "offset": 112
+ },
+ "end": {
+ "line": 5,
+ "column": 43,
+ "offset": 153
+ }
+ },
+ "description": "```go\npackage a (\"golang.org/x/tools/internal/lsp/godef/a\")\n```\n\n[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a)"
+}
+
+-- AImport-hover --
+[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a)
+
+```go
+package a ("golang.org/x/tools/internal/lsp/godef/a")
+```
+-- AString-definition --
+godef/a/a.go:16:6-7: defined here as ```go
+A string //@mark(AString, "A")
+
+```
+
+[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)
+-- AString-definition-json --
+{
+ "span": {
+ "uri": "file://godef/a/a.go",
+ "start": {
+ "line": 16,
+ "column": 6,
+ "offset": 159
+ },
+ "end": {
+ "line": 16,
+ "column": 7,
+ "offset": 160
+ }
+ },
+ "description": "```go\nA string //@mark(AString, \"A\")\n\n```\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)"
+}
+
+-- AString-hover --
+[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)
+
+```go
+A string //@mark(AString, "A")
+
+```
+-- AStuff-definition --
+godef/a/a.go:18:6-12: defined here as ```go
+func a.AStuff()
+```
+
+[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff)
+-- AStuff-definition-json --
+{
+ "span": {
+ "uri": "file://godef/a/a.go",
+ "start": {
+ "line": 18,
+ "column": 6,
+ "offset": 196
+ },
+ "end": {
+ "line": 18,
+ "column": 12,
+ "offset": 202
+ }
+ },
+ "description": "```go\nfunc a.AStuff()\n```\n\n[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff)"
+}
+
+-- AStuff-hover --
+[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff)
+
+```go
+func a.AStuff()
+```
+-- S1-definition --
+godef/b/b.go:8:6-8: defined here as ```go
+S1 struct {
+ F1 int //@mark(S1F1, "F1")
+ S2 //@godef("S2", S2), mark(S1S2, "S2")
+ a.A //@godef("A", AString)
+}
+```
+
+[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)
+-- S1-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 8,
+ "column": 6,
+ "offset": 193
+ },
+ "end": {
+ "line": 8,
+ "column": 8,
+ "offset": 195
+ }
+ },
+ "description": "```go\nS1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2), mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)"
+}
+
+-- S1-hover --
+[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)
+
+```go
+S1 struct {
+ F1 int //@mark(S1F1, "F1")
+ S2 //@godef("S2", S2), mark(S1S2, "S2")
+ a.A //@godef("A", AString)
+}
+```
+-- S1F1-definition --
+godef/b/b.go:9:2-4: defined here as ```go
+field F1 int
+```
+
+[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)
+
+\@mark\(S1F1, \"F1\"\)
+-- S1F1-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 9,
+ "column": 2,
+ "offset": 212
+ },
+ "end": {
+ "line": 9,
+ "column": 4,
+ "offset": 214
+ }
+ },
+ "description": "```go\nfield F1 int\n```\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)"
+}
+
+-- S1F1-hover --
+\@mark\(S1F1, \"F1\"\)
+
+[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)
+
+```go
+field F1 int
+```
+-- S1S2-definition --
+godef/b/b.go:10:2-4: defined here as ```go
+field S2 S2
+```
+
+[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)
+
+\@godef\(\"S2\", S2\), mark\(S1S2, \"S2\"\)
+-- S1S2-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 10,
+ "column": 2,
+ "offset": 241
+ },
+ "end": {
+ "line": 10,
+ "column": 4,
+ "offset": 243
+ }
+ },
+ "description": "```go\nfield S2 S2\n```\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)\n\n\\@godef\\(\\\"S2\\\", S2\\), mark\\(S1S2, \\\"S2\\\"\\)"
+}
+
+-- S1S2-hover --
+\@godef\(\"S2\", S2\), mark\(S1S2, \"S2\"\)
+
+[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)
+
+```go
+field S2 S2
+```
+-- S2-definition --
+godef/b/b.go:14:6-8: defined here as ```go
+S2 struct {
+ F1 string //@mark(S2F1, "F1")
+ F2 int //@mark(S2F2, "F2")
+ *a.A //@godef("A", AString),godef("a",AImport)
+}
+```
+
+[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2)
+-- S2-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 14,
+ "column": 6,
+ "offset": 326
+ },
+ "end": {
+ "line": 14,
+ "column": 8,
+ "offset": 328
+ }
+ },
+ "description": "```go\nS2 struct {\n\tF1 string //@mark(S2F1, \"F1\")\n\tF2 int //@mark(S2F2, \"F2\")\n\t*a.A //@godef(\"A\", AString),godef(\"a\",AImport)\n}\n```\n\n[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2)"
+}
+
+-- S2-hover --
+[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2)
+
+```go
+S2 struct {
+ F1 string //@mark(S2F1, "F1")
+ F2 int //@mark(S2F2, "F2")
+ *a.A //@godef("A", AString),godef("a",AImport)
+}
+```
+-- S2F1-definition --
+godef/b/b.go:15:2-4: defined here as ```go
+field F1 string
+```
+
+[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)
+
+\@mark\(S2F1, \"F1\"\)
+-- S2F1-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 15,
+ "column": 2,
+ "offset": 345
+ },
+ "end": {
+ "line": 15,
+ "column": 4,
+ "offset": 347
+ }
+ },
+ "description": "```go\nfield F1 string\n```\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)\n\n\\@mark\\(S2F1, \\\"F1\\\"\\)"
+}
+
+-- S2F1-hover --
+\@mark\(S2F1, \"F1\"\)
+
+[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)
+
+```go
+field F1 string
+```
+-- S2F2-definition --
+godef/b/b.go:16:2-4: defined here as ```go
+field F2 int
+```
+
+[`(b.S1).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F2)
+
+\@mark\(S2F2, \"F2\"\)
+-- S2F2-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 16,
+ "column": 2,
+ "offset": 378
+ },
+ "end": {
+ "line": 16,
+ "column": 4,
+ "offset": 380
+ }
+ },
+ "description": "```go\nfield F2 int\n```\n\n[`(b.S1).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F2)\n\n\\@mark\\(S2F2, \\\"F2\\\"\\)"
+}
+
+-- S2F2-hover --
+\@mark\(S2F2, \"F2\"\)
+
+[`(b.S1).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F2)
+
+```go
+field F2 int
+```
+-- bX-definition --
+godef/b/b.go:37:7-8: defined here as ```go
+const X untyped int = 0
+```
+
+[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)
+-- bX-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 37,
+ "column": 7,
+ "offset": 813
+ },
+ "end": {
+ "line": 37,
+ "column": 8,
+ "offset": 814
+ }
+ },
+ "description": "```go\nconst X untyped int = 0\n```\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)"
+}
+
+-- bX-hover --
+[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)
+
+```go
+const X untyped int = 0
+```
+-- myFoo-definition --
+godef/b/b.go:4:2-7: defined here as ```go
+package myFoo ("golang.org/x/tools/internal/lsp/foo")
+```
+
+[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo)
+-- myFoo-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 4,
+ "column": 2,
+ "offset": 21
+ },
+ "end": {
+ "line": 4,
+ "column": 7,
+ "offset": 26
+ }
+ },
+ "description": "```go\npackage myFoo (\"golang.org/x/tools/internal/lsp/foo\")\n```\n\n[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo)"
+}
+
+-- myFoo-hover --
+[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo)
+
+```go
+package myFoo ("golang.org/x/tools/internal/lsp/foo")
+```
diff --git a/internal/lsp/testdata/godef/b/c.go b/internal/lsp/testdata/lsp/primarymod/godef/b/c.go
similarity index 100%
rename from internal/lsp/testdata/godef/b/c.go
rename to internal/lsp/testdata/lsp/primarymod/godef/b/c.go
diff --git a/internal/lsp/testdata/lsp/primarymod/godef/b/c.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/b/c.go.golden
new file mode 100644
index 0000000..7711d29
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/godef/b/c.go.golden
@@ -0,0 +1,72 @@
+-- S1-definition --
+godef/b/b.go:8:6-8: defined here as ```go
+S1 struct {
+ F1 int //@mark(S1F1, "F1")
+ S2 //@godef("S2", S2), mark(S1S2, "S2")
+ a.A //@godef("A", AString)
+}
+```
+
+[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)
+-- S1-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 8,
+ "column": 6,
+ "offset": 193
+ },
+ "end": {
+ "line": 8,
+ "column": 8,
+ "offset": 195
+ }
+ },
+ "description": "```go\nS1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2), mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)"
+}
+
+-- S1-hover --
+[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)
+
+```go
+S1 struct {
+ F1 int //@mark(S1F1, "F1")
+ S2 //@godef("S2", S2), mark(S1S2, "S2")
+ a.A //@godef("A", AString)
+}
+```
+-- S1F1-definition --
+godef/b/b.go:9:2-4: defined here as ```go
+field F1 int
+```
+
+[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)
+
+\@mark\(S1F1, \"F1\"\)
+-- S1F1-definition-json --
+{
+ "span": {
+ "uri": "file://godef/b/b.go",
+ "start": {
+ "line": 9,
+ "column": 2,
+ "offset": 212
+ },
+ "end": {
+ "line": 9,
+ "column": 4,
+ "offset": 214
+ }
+ },
+ "description": "```go\nfield F1 int\n```\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)"
+}
+
+-- S1F1-hover --
+\@mark\(S1F1, \"F1\"\)
+
+[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)
+
+```go
+field F1 int
+```
diff --git a/internal/lsp/testdata/godef/b/c.go.saved b/internal/lsp/testdata/lsp/primarymod/godef/b/c.go.saved
similarity index 100%
rename from internal/lsp/testdata/godef/b/c.go.saved
rename to internal/lsp/testdata/lsp/primarymod/godef/b/c.go.saved
diff --git a/internal/lsp/testdata/godef/b/e.go b/internal/lsp/testdata/lsp/primarymod/godef/b/e.go
similarity index 100%
rename from internal/lsp/testdata/godef/b/e.go
rename to internal/lsp/testdata/lsp/primarymod/godef/b/e.go
diff --git a/internal/lsp/testdata/godef/b/e.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/b/e.go.golden
similarity index 61%
rename from internal/lsp/testdata/godef/b/e.go.golden
rename to internal/lsp/testdata/lsp/primarymod/godef/b/e.go.golden
index df38081..24026cb 100644
--- a/internal/lsp/testdata/godef/b/e.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/godef/b/e.go.golden
@@ -1,11 +1,11 @@
-- Member-definition --
-godef/a/d.go:6:2-8: defined here as \@Member
+godef/a/d.go:6:2-8: defined here as ```go
+field Member string
+```
[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)
-```go
-field Member string
-```
+\@Member
-- Member-definition-json --
{
"span": {
@@ -21,7 +21,7 @@
"offset": 61
}
},
- "description": "\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)\n\n```go\nfield Member string\n```"
+ "description": "```go\nfield Member string\n```\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)\n\n\\@Member"
}
-- Member-hover --
@@ -33,11 +33,11 @@
field Member string
```
-- Other-definition --
-godef/a/d.go:9:5-10: defined here as [`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)
-
-```go
+godef/a/d.go:9:5-10: defined here as ```go
var a.Other a.Thing
```
+
+[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)
-- Other-definition-json --
{
"span": {
@@ -53,7 +53,7 @@
"offset": 91
}
},
- "description": "[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\n\n```go\nvar a.Other a.Thing\n```"
+ "description": "```go\nvar a.Other a.Thing\n```\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"
}
-- Other-hover --
@@ -63,13 +63,13 @@
var a.Other a.Thing
```
-- Thing-definition --
-godef/a/d.go:5:6-11: defined here as [`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)
-
-```go
+godef/a/d.go:5:6-11: defined here as ```go
Thing struct {
Member string //@Member
}
```
+
+[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)
-- Thing-definition-json --
{
"span": {
@@ -85,7 +85,7 @@
"offset": 35
}
},
- "description": "[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)\n\n```go\nThing struct {\n\tMember string //@Member\n}\n```"
+ "description": "```go\nThing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)"
}
-- Thing-hover --
@@ -97,11 +97,11 @@
}
```
-- Things-definition --
-godef/a/d.go:11:6-12: defined here as [`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)
-
-```go
+godef/a/d.go:11:6-12: defined here as ```go
func a.Things(val []string) []a.Thing
```
+
+[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)
-- Things-definition-json --
{
"span": {
@@ -117,7 +117,7 @@
"offset": 119
}
},
- "description": "[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)\n\n```go\nfunc a.Things(val []string) []a.Thing\n```"
+ "description": "```go\nfunc a.Things(val []string) []a.Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)"
}
-- Things-hover --
diff --git a/internal/lsp/testdata/godef/broken/unclosedIf.go.golden b/internal/lsp/testdata/lsp/primarymod/godef/broken/unclosedIf.go.golden
similarity index 100%
rename from internal/lsp/testdata/godef/broken/unclosedIf.go.golden
rename to internal/lsp/testdata/lsp/primarymod/godef/broken/unclosedIf.go.golden
diff --git a/internal/lsp/testdata/godef/broken/unclosedIf.go.in b/internal/lsp/testdata/lsp/primarymod/godef/broken/unclosedIf.go.in
similarity index 100%
rename from internal/lsp/testdata/godef/broken/unclosedIf.go.in
rename to internal/lsp/testdata/lsp/primarymod/godef/broken/unclosedIf.go.in
diff --git a/internal/lsp/testdata/good/good0.go b/internal/lsp/testdata/lsp/primarymod/good/good0.go
similarity index 71%
rename from internal/lsp/testdata/good/good0.go
rename to internal/lsp/testdata/lsp/primarymod/good/good0.go
index 85c46aa..89450a8 100644
--- a/internal/lsp/testdata/good/good0.go
+++ b/internal/lsp/testdata/lsp/primarymod/good/good0.go
@@ -1,4 +1,4 @@
-package good //@diag("package", "no_diagnostics", "")
+package good //@diag("package", "no_diagnostics", "", "error")
func stuff() { //@item(good_stuff, "stuff", "func()", "func"),prepare("stu", "stuff", "stuff")
x := 5
diff --git a/internal/lsp/testdata/good/good1.go b/internal/lsp/testdata/lsp/primarymod/good/good1.go
similarity index 83%
rename from internal/lsp/testdata/good/good1.go
rename to internal/lsp/testdata/lsp/primarymod/good/good1.go
index 826b114..b6180eb 100644
--- a/internal/lsp/testdata/good/good1.go
+++ b/internal/lsp/testdata/lsp/primarymod/good/good1.go
@@ -1,8 +1,8 @@
-package good //@diag("package", "no_diagnostics", "")
+package good //@diag("package", "no_diagnostics", "", "error")
import (
_ "go/ast" //@prepare("go/ast", "_", "_")
- "golang.org/x/tools/internal/lsp/types" //@item(types_import, "types", "\"golang.org/x/tools/internal/lsp/types\"", "package"),prepare("types","\"", "types")
+ "golang.org/x/tools/internal/lsp/types" //@item(types_import, "types", "\"golang.org/x/tools/internal/lsp/types\"", "package")
)
func random() int { //@item(good_random, "random", "func() int", "func")
diff --git a/internal/lsp/testdata/highlights/highlights.go b/internal/lsp/testdata/lsp/primarymod/highlights/highlights.go
similarity index 89%
rename from internal/lsp/testdata/highlights/highlights.go
rename to internal/lsp/testdata/lsp/primarymod/highlights/highlights.go
index de67efe..db09b56 100644
--- a/internal/lsp/testdata/highlights/highlights.go
+++ b/internal/lsp/testdata/lsp/primarymod/highlights/highlights.go
@@ -4,8 +4,6 @@
"fmt" //@mark(fmtImp, "\"fmt\""),highlight(fmtImp, fmtImp, fmt1, fmt2, fmt3, fmt4)
h2 "net/http" //@mark(hImp, "h2"),highlight(hImp, hImp, hUse)
"sort"
-
- "golang.org/x/tools/internal/lsp/protocol"
)
type F struct{ bar int } //@mark(barDeclaration, "bar"),highlight(barDeclaration, barDeclaration, bar1, bar2, bar3)
@@ -35,12 +33,10 @@
Print() //@mark(printTest, "Print"),highlight(printTest, printFunc, printTest)
}
-func toProtocolHighlight(rngs []protocol.Range) []protocol.DocumentHighlight { //@mark(doc1, "DocumentHighlight"),mark(docRet1, "[]protocol.DocumentHighlight"),highlight(doc1, docRet1, doc1, doc2, doc3, result)
- result := make([]protocol.DocumentHighlight, 0, len(rngs)) //@mark(doc2, "DocumentHighlight"),highlight(doc2, doc1, doc2, doc3)
- kind := protocol.Text
+func toProtocolHighlight(rngs []int) []DocumentHighlight { //@mark(doc1, "DocumentHighlight"),mark(docRet1, "[]DocumentHighlight"),highlight(doc1, docRet1, doc1, doc2, doc3, result)
+ result := make([]DocumentHighlight, 0, len(rngs)) //@mark(doc2, "DocumentHighlight"),highlight(doc2, doc1, doc2, doc3)
for _, rng := range rngs {
- result = append(result, protocol.DocumentHighlight{ //@mark(doc3, "DocumentHighlight"),highlight(doc3, doc1, doc2, doc3)
- Kind: kind,
+ result = append(result, DocumentHighlight{ //@mark(doc3, "DocumentHighlight"),highlight(doc3, doc1, doc2, doc3)
Range: rng,
})
}
diff --git a/internal/lsp/testdata/implementation/implementation.go b/internal/lsp/testdata/lsp/primarymod/implementation/implementation.go
similarity index 100%
rename from internal/lsp/testdata/implementation/implementation.go
rename to internal/lsp/testdata/lsp/primarymod/implementation/implementation.go
diff --git a/internal/lsp/testdata/implementation/other/other.go b/internal/lsp/testdata/lsp/primarymod/implementation/other/other.go
similarity index 100%
rename from internal/lsp/testdata/implementation/other/other.go
rename to internal/lsp/testdata/lsp/primarymod/implementation/other/other.go
diff --git a/internal/lsp/testdata/implementation/other/other_test.go b/internal/lsp/testdata/lsp/primarymod/implementation/other/other_test.go
similarity index 100%
rename from internal/lsp/testdata/implementation/other/other_test.go
rename to internal/lsp/testdata/lsp/primarymod/implementation/other/other_test.go
diff --git a/internal/lsp/testdata/importedcomplit/imported_complit.go b/internal/lsp/testdata/lsp/primarymod/importedcomplit/imported_complit.go
similarity index 100%
rename from internal/lsp/testdata/importedcomplit/imported_complit.go
rename to internal/lsp/testdata/lsp/primarymod/importedcomplit/imported_complit.go
diff --git a/internal/lsp/testdata/imports/add_import.go.golden b/internal/lsp/testdata/lsp/primarymod/imports/add_import.go.golden
similarity index 100%
rename from internal/lsp/testdata/imports/add_import.go.golden
rename to internal/lsp/testdata/lsp/primarymod/imports/add_import.go.golden
diff --git a/internal/lsp/testdata/imports/add_import.go.in b/internal/lsp/testdata/lsp/primarymod/imports/add_import.go.in
similarity index 100%
rename from internal/lsp/testdata/imports/add_import.go.in
rename to internal/lsp/testdata/lsp/primarymod/imports/add_import.go.in
diff --git a/internal/lsp/testdata/imports/good_imports.go.golden b/internal/lsp/testdata/lsp/primarymod/imports/good_imports.go.golden
similarity index 100%
rename from internal/lsp/testdata/imports/good_imports.go.golden
rename to internal/lsp/testdata/lsp/primarymod/imports/good_imports.go.golden
diff --git a/internal/lsp/testdata/imports/good_imports.go.in b/internal/lsp/testdata/lsp/primarymod/imports/good_imports.go.in
similarity index 100%
rename from internal/lsp/testdata/imports/good_imports.go.in
rename to internal/lsp/testdata/lsp/primarymod/imports/good_imports.go.in
diff --git a/internal/lsp/testdata/imports/issue35458.go.golden b/internal/lsp/testdata/lsp/primarymod/imports/issue35458.go.golden
similarity index 100%
rename from internal/lsp/testdata/imports/issue35458.go.golden
rename to internal/lsp/testdata/lsp/primarymod/imports/issue35458.go.golden
diff --git a/internal/lsp/testdata/imports/issue35458.go.in b/internal/lsp/testdata/lsp/primarymod/imports/issue35458.go.in
similarity index 100%
rename from internal/lsp/testdata/imports/issue35458.go.in
rename to internal/lsp/testdata/lsp/primarymod/imports/issue35458.go.in
diff --git a/internal/lsp/testdata/imports/multiple_blocks.go.golden b/internal/lsp/testdata/lsp/primarymod/imports/multiple_blocks.go.golden
similarity index 100%
rename from internal/lsp/testdata/imports/multiple_blocks.go.golden
rename to internal/lsp/testdata/lsp/primarymod/imports/multiple_blocks.go.golden
diff --git a/internal/lsp/testdata/imports/multiple_blocks.go.in b/internal/lsp/testdata/lsp/primarymod/imports/multiple_blocks.go.in
similarity index 100%
rename from internal/lsp/testdata/imports/multiple_blocks.go.in
rename to internal/lsp/testdata/lsp/primarymod/imports/multiple_blocks.go.in
diff --git a/internal/lsp/testdata/imports/needs_imports.go.golden b/internal/lsp/testdata/lsp/primarymod/imports/needs_imports.go.golden
similarity index 100%
rename from internal/lsp/testdata/imports/needs_imports.go.golden
rename to internal/lsp/testdata/lsp/primarymod/imports/needs_imports.go.golden
diff --git a/internal/lsp/testdata/imports/needs_imports.go.in b/internal/lsp/testdata/lsp/primarymod/imports/needs_imports.go.in
similarity index 100%
rename from internal/lsp/testdata/imports/needs_imports.go.in
rename to internal/lsp/testdata/lsp/primarymod/imports/needs_imports.go.in
diff --git a/internal/lsp/testdata/imports/remove_import.go.golden b/internal/lsp/testdata/lsp/primarymod/imports/remove_import.go.golden
similarity index 100%
rename from internal/lsp/testdata/imports/remove_import.go.golden
rename to internal/lsp/testdata/lsp/primarymod/imports/remove_import.go.golden
diff --git a/internal/lsp/testdata/imports/remove_import.go.in b/internal/lsp/testdata/lsp/primarymod/imports/remove_import.go.in
similarity index 100%
rename from internal/lsp/testdata/imports/remove_import.go.in
rename to internal/lsp/testdata/lsp/primarymod/imports/remove_import.go.in
diff --git a/internal/lsp/testdata/imports/remove_imports.go.golden b/internal/lsp/testdata/lsp/primarymod/imports/remove_imports.go.golden
similarity index 100%
rename from internal/lsp/testdata/imports/remove_imports.go.golden
rename to internal/lsp/testdata/lsp/primarymod/imports/remove_imports.go.golden
diff --git a/internal/lsp/testdata/imports/remove_imports.go.in b/internal/lsp/testdata/lsp/primarymod/imports/remove_imports.go.in
similarity index 100%
rename from internal/lsp/testdata/imports/remove_imports.go.in
rename to internal/lsp/testdata/lsp/primarymod/imports/remove_imports.go.in
diff --git a/internal/lsp/testdata/index/index.go b/internal/lsp/testdata/lsp/primarymod/index/index.go
similarity index 100%
rename from internal/lsp/testdata/index/index.go
rename to internal/lsp/testdata/lsp/primarymod/index/index.go
diff --git a/internal/lsp/testdata/interfacerank/interface_rank.go b/internal/lsp/testdata/lsp/primarymod/interfacerank/interface_rank.go
similarity index 100%
rename from internal/lsp/testdata/interfacerank/interface_rank.go
rename to internal/lsp/testdata/lsp/primarymod/interfacerank/interface_rank.go
diff --git a/internal/lsp/testdata/keywords/accidental_keywords.go.in b/internal/lsp/testdata/lsp/primarymod/keywords/accidental_keywords.go.in
similarity index 83%
rename from internal/lsp/testdata/keywords/accidental_keywords.go.in
rename to internal/lsp/testdata/lsp/primarymod/keywords/accidental_keywords.go.in
index 22ad4e2..711841c 100644
--- a/internal/lsp/testdata/keywords/accidental_keywords.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/keywords/accidental_keywords.go.in
@@ -18,6 +18,12 @@
}
func _() {
+ channel := 123 //@item(kwChannel, "channel", "int", "var")
+ chan //@complete(" //", kwChannel)
+ foo.bar()
+}
+
+func _() {
foo.bar()
var typeName string //@item(kwTypeName, "typeName", "string", "var")
foo.bar()
diff --git a/internal/lsp/testdata/keywords/keywords.go b/internal/lsp/testdata/lsp/primarymod/keywords/keywords.go
similarity index 92%
rename from internal/lsp/testdata/keywords/keywords.go
rename to internal/lsp/testdata/lsp/primarymod/keywords/keywords.go
index 5fda68d..7e7fd5b 100644
--- a/internal/lsp/testdata/keywords/keywords.go
+++ b/internal/lsp/testdata/lsp/primarymod/keywords/keywords.go
@@ -1,5 +1,7 @@
package keywords
+//@rank("", type),rank("", func),rank("", var),rank("", const),rank("", import)
+
func _() {
var test int
var tChan chan int
@@ -42,7 +44,7 @@
f //@complete(" //", for)
d //@complete(" //", defer)
- g //@complete(" //", go)
+ g //@rank(" //", go),rank(" //", goto)
r //@complete(" //", return)
i //@complete(" //", if)
e //@complete(" //", else)
@@ -72,3 +74,4 @@
/* return */ //@item(return, "return", "", "keyword")
/* var */ //@item(var, "var", "", "keyword")
/* const */ //@item(const, "const", "", "keyword")
+/* goto */ //@item(goto, "goto", "", "keyword")
diff --git a/internal/lsp/testdata/labels/labels.go b/internal/lsp/testdata/lsp/primarymod/labels/labels.go
similarity index 100%
rename from internal/lsp/testdata/labels/labels.go
rename to internal/lsp/testdata/lsp/primarymod/labels/labels.go
diff --git a/internal/lsp/testdata/links/links.go b/internal/lsp/testdata/lsp/primarymod/links/links.go
similarity index 87%
rename from internal/lsp/testdata/links/links.go
rename to internal/lsp/testdata/lsp/primarymod/links/links.go
index 926b603..be15ddb 100644
--- a/internal/lsp/testdata/links/links.go
+++ b/internal/lsp/testdata/lsp/primarymod/links/links.go
@@ -7,7 +7,7 @@
_ "database/sql" //@link(`database/sql`, `https://pkg.go.dev/database/sql`)
- errors "golang.org/x/xerrors" //@link(`golang.org/x/xerrors`, `https://pkg.go.dev/golang.org/x/xerrors`)
+ _ "example.com/extramodule/pkg" //@link(`example.com/extramodule/pkg`,`https://pkg.go.dev/example.com/extramodule@v1.0.0/pkg`)
)
var (
diff --git a/internal/lsp/testdata/maps/maps.go.in b/internal/lsp/testdata/lsp/primarymod/maps/maps.go.in
similarity index 63%
rename from internal/lsp/testdata/maps/maps.go.in
rename to internal/lsp/testdata/lsp/primarymod/maps/maps.go.in
index 5c9dedd..b4a4cdd 100644
--- a/internal/lsp/testdata/maps/maps.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/maps/maps.go.in
@@ -11,8 +11,8 @@
// comparable
type aStruct struct{} //@item(mapStructType, "aStruct", "struct{...}", "struct")
- map[]a{} //@complete("]", mapSliceTypePtr, mapStructType, mapVar)
+ map[]a{} //@complete("]", mapSliceTypePtr, mapStructType)
- map[a]a{} //@complete("]", mapSliceTypePtr, mapStructType, mapVar)
- map[a]a{} //@complete("{", mapSliceType, mapStructType, mapVar)
+ map[a]a{} //@complete("]", mapSliceTypePtr, mapStructType)
+ map[a]a{} //@complete("{", mapSliceType, mapStructType)
}
diff --git a/internal/lsp/testdata/lsp/primarymod/multireturn/multi_return.go b/internal/lsp/testdata/lsp/primarymod/multireturn/multi_return.go
new file mode 100644
index 0000000..0da698f
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/multireturn/multi_return.go
@@ -0,0 +1,36 @@
+package multireturn
+
+func f0() {} //@item(multiF0, "f0", "func()", "func")
+
+func f1(int) int { return 0 } //@item(multiF1, "f1", "func(int) int", "func")
+
+func f2(int, int) (int, int) { return 0, 0 } //@item(multiF2, "f2", "func(int, int) (int, int)", "func")
+
+func f2Str(string, string) (string, string) { return "", "" } //@item(multiF2Str, "f2Str", "func(string, string) (string, string)", "func")
+
+func f3(int, int, int) (int, int, int) { return 0, 0, 0 } //@item(multiF3, "f3", "func(int, int, int) (int, int, int)", "func")
+
+func _() {
+ _ := f //@rank(" //", multiF1, multiF2)
+
+ _, _ := f //@rank(" //", multiF2, multiF0),rank(" //", multiF1, multiF0)
+
+ _, _ := _, f //@rank(" //", multiF1, multiF2),rank(" //", multiF1, multiF0)
+
+ _, _ := f, abc //@rank(", abc", multiF1, multiF2)
+
+ f1() //@rank(")", multiF1, multiF0)
+ f1(f) //@rank(")", multiF1, multiF2)
+ f2(f) //@rank(")", multiF2, multiF3),rank(")", multiF1, multiF3)
+ f2(1, f) //@rank(")", multiF1, multiF2),rank(")", multiF1, multiF0)
+ f2Str() //@rank(")", multiF2Str, multiF2)
+
+ var i int
+ i, _ := f //@rank(" //", multiF2, multiF2Str)
+
+ var s string
+ _, s := f //@rank(" //", multiF2Str, multiF2)
+
+ var variadic func(int, ...int)
+ variadic() //@rank(")", multiF1, multiF0),rank(")", multiF2, multiF0),rank(")", multiF3, multiF0)
+}
diff --git a/internal/lsp/testdata/nested_complit/nested_complit.go.in b/internal/lsp/testdata/lsp/primarymod/nested_complit/nested_complit.go.in
similarity index 100%
rename from internal/lsp/testdata/nested_complit/nested_complit.go.in
rename to internal/lsp/testdata/lsp/primarymod/nested_complit/nested_complit.go.in
diff --git a/internal/lsp/testdata/nodisk/empty b/internal/lsp/testdata/lsp/primarymod/nodisk/empty
similarity index 100%
rename from internal/lsp/testdata/nodisk/empty
rename to internal/lsp/testdata/lsp/primarymod/nodisk/empty
diff --git a/internal/lsp/testdata/nodisk/nodisk.overlay.go b/internal/lsp/testdata/lsp/primarymod/nodisk/nodisk.overlay.go
similarity index 100%
rename from internal/lsp/testdata/nodisk/nodisk.overlay.go
rename to internal/lsp/testdata/lsp/primarymod/nodisk/nodisk.overlay.go
diff --git a/internal/lsp/testdata/noparse/noparse.go.in b/internal/lsp/testdata/lsp/primarymod/noparse/noparse.go.in
similarity index 92%
rename from internal/lsp/testdata/noparse/noparse.go.in
rename to internal/lsp/testdata/lsp/primarymod/noparse/noparse.go.in
index 66a8cce..7dc23e0 100644
--- a/internal/lsp/testdata/noparse/noparse.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/noparse/noparse.go.in
@@ -8,4 +8,4 @@
x := 5
}
-func .() {} //@diag(".", "syntax", "expected 'IDENT', found '.'")
+func .() {} //@diag(".", "syntax", "expected 'IDENT', found '.'", "error")
diff --git a/internal/lsp/testdata/noparse_format/noparse_format.go.golden b/internal/lsp/testdata/lsp/primarymod/noparse_format/noparse_format.go.golden
similarity index 100%
rename from internal/lsp/testdata/noparse_format/noparse_format.go.golden
rename to internal/lsp/testdata/lsp/primarymod/noparse_format/noparse_format.go.golden
diff --git a/internal/lsp/testdata/noparse_format/noparse_format.go.in b/internal/lsp/testdata/lsp/primarymod/noparse_format/noparse_format.go.in
similarity index 89%
rename from internal/lsp/testdata/noparse_format/noparse_format.go.in
rename to internal/lsp/testdata/lsp/primarymod/noparse_format/noparse_format.go.in
index f230a69..4fc3824 100644
--- a/internal/lsp/testdata/noparse_format/noparse_format.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/noparse_format/noparse_format.go.in
@@ -4,6 +4,6 @@
func what() {
var b int
- if { hi() //@diag("{", "syntax", "missing condition in if statement")
+ if { hi() //@diag("{", "syntax", "missing condition in if statement", "error")
}
}
\ No newline at end of file
diff --git a/internal/lsp/testdata/noparse_format/parse_format.go.golden b/internal/lsp/testdata/lsp/primarymod/noparse_format/parse_format.go.golden
similarity index 100%
rename from internal/lsp/testdata/noparse_format/parse_format.go.golden
rename to internal/lsp/testdata/lsp/primarymod/noparse_format/parse_format.go.golden
diff --git a/internal/lsp/testdata/noparse_format/parse_format.go.in b/internal/lsp/testdata/lsp/primarymod/noparse_format/parse_format.go.in
similarity index 100%
rename from internal/lsp/testdata/noparse_format/parse_format.go.in
rename to internal/lsp/testdata/lsp/primarymod/noparse_format/parse_format.go.in
diff --git a/internal/lsp/testdata/rank/assign_rank.go.in b/internal/lsp/testdata/lsp/primarymod/rank/assign_rank.go.in
similarity index 100%
rename from internal/lsp/testdata/rank/assign_rank.go.in
rename to internal/lsp/testdata/lsp/primarymod/rank/assign_rank.go.in
diff --git a/internal/lsp/testdata/rank/binexpr_rank.go.in b/internal/lsp/testdata/lsp/primarymod/rank/binexpr_rank.go.in
similarity index 100%
rename from internal/lsp/testdata/rank/binexpr_rank.go.in
rename to internal/lsp/testdata/lsp/primarymod/rank/binexpr_rank.go.in
diff --git a/internal/lsp/testdata/rank/convert_rank.go.in b/internal/lsp/testdata/lsp/primarymod/rank/convert_rank.go.in
similarity index 90%
rename from internal/lsp/testdata/rank/convert_rank.go.in
rename to internal/lsp/testdata/lsp/primarymod/rank/convert_rank.go.in
index 77e2d27..77850ef 100644
--- a/internal/lsp/testdata/rank/convert_rank.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/rank/convert_rank.go.in
@@ -39,4 +39,7 @@
type myUint uint32
var mu myUint
mu = conv //@rank(" //", convertD, convertE)
+
+ // don't downrank constants when assigning to interface{}
+ var _ interface{} = c //@rank(" //", convertD, complex)
}
diff --git a/internal/lsp/testdata/rank/switch_rank.go.in b/internal/lsp/testdata/lsp/primarymod/rank/switch_rank.go.in
similarity index 100%
rename from internal/lsp/testdata/rank/switch_rank.go.in
rename to internal/lsp/testdata/lsp/primarymod/rank/switch_rank.go.in
diff --git a/internal/lsp/testdata/rank/type_assert_rank.go.in b/internal/lsp/testdata/lsp/primarymod/rank/type_assert_rank.go.in
similarity index 70%
rename from internal/lsp/testdata/rank/type_assert_rank.go.in
rename to internal/lsp/testdata/lsp/primarymod/rank/type_assert_rank.go.in
index 3490c85..416541c 100644
--- a/internal/lsp/testdata/rank/type_assert_rank.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/rank/type_assert_rank.go.in
@@ -4,5 +4,5 @@
type flower int //@item(flower, "flower", "int", "type")
var fig string //@item(fig, "fig", "string", "var")
- _ = interface{}(nil).(f) //@complete(") //", flower, fig)
+ _ = interface{}(nil).(f) //@complete(") //", flower)
}
diff --git a/internal/lsp/testdata/rank/type_switch_rank.go.in b/internal/lsp/testdata/lsp/primarymod/rank/type_switch_rank.go.in
similarity index 68%
rename from internal/lsp/testdata/rank/type_switch_rank.go.in
rename to internal/lsp/testdata/lsp/primarymod/rank/type_switch_rank.go.in
index 6cec597..293025f 100644
--- a/internal/lsp/testdata/rank/type_switch_rank.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/rank/type_switch_rank.go.in
@@ -5,7 +5,7 @@
var banana string //@item(banana, "banana", "string", "var")
switch interface{}(pear).(type) {
- case b: //@complete(":", basket, banana)
- b //@complete(" //", banana, basket, break)
+ case b: //@complete(":", basket)
+ b //@complete(" //", banana, basket)
}
}
diff --git a/internal/lsp/testdata/references/other/other.go b/internal/lsp/testdata/lsp/primarymod/references/other/other.go
similarity index 100%
rename from internal/lsp/testdata/references/other/other.go
rename to internal/lsp/testdata/lsp/primarymod/references/other/other.go
diff --git a/internal/lsp/testdata/references/refs.go b/internal/lsp/testdata/lsp/primarymod/references/refs.go
similarity index 91%
rename from internal/lsp/testdata/references/refs.go
rename to internal/lsp/testdata/lsp/primarymod/references/refs.go
index 019baf8..6ce4afc 100644
--- a/internal/lsp/testdata/references/refs.go
+++ b/internal/lsp/testdata/lsp/primarymod/references/refs.go
@@ -1,3 +1,4 @@
+// Package refs is a package used to test find references.
package refs
type i int //@mark(typeI, "i"),refs("i", typeI, argI, returnI, embeddedI)
diff --git a/internal/lsp/testdata/references/refs_test.go b/internal/lsp/testdata/lsp/primarymod/references/refs_test.go
similarity index 100%
rename from internal/lsp/testdata/references/refs_test.go
rename to internal/lsp/testdata/lsp/primarymod/references/refs_test.go
diff --git a/internal/lsp/testdata/rename/a/random.go.golden b/internal/lsp/testdata/lsp/primarymod/rename/a/random.go.golden
similarity index 100%
rename from internal/lsp/testdata/rename/a/random.go.golden
rename to internal/lsp/testdata/lsp/primarymod/rename/a/random.go.golden
diff --git a/internal/lsp/testdata/rename/a/random.go.in b/internal/lsp/testdata/lsp/primarymod/rename/a/random.go.in
similarity index 100%
rename from internal/lsp/testdata/rename/a/random.go.in
rename to internal/lsp/testdata/lsp/primarymod/rename/a/random.go.in
diff --git a/internal/lsp/testdata/rename/b/b.go b/internal/lsp/testdata/lsp/primarymod/rename/b/b.go
similarity index 100%
rename from internal/lsp/testdata/rename/b/b.go
rename to internal/lsp/testdata/lsp/primarymod/rename/b/b.go
diff --git a/internal/lsp/testdata/rename/b/b.go.golden b/internal/lsp/testdata/lsp/primarymod/rename/b/b.go.golden
similarity index 100%
rename from internal/lsp/testdata/rename/b/b.go.golden
rename to internal/lsp/testdata/lsp/primarymod/rename/b/b.go.golden
diff --git a/internal/lsp/testdata/rename/bad/bad.go.golden b/internal/lsp/testdata/lsp/primarymod/rename/bad/bad.go.golden
similarity index 100%
rename from internal/lsp/testdata/rename/bad/bad.go.golden
rename to internal/lsp/testdata/lsp/primarymod/rename/bad/bad.go.golden
diff --git a/internal/lsp/testdata/rename/bad/bad.go.in b/internal/lsp/testdata/lsp/primarymod/rename/bad/bad.go.in
similarity index 100%
rename from internal/lsp/testdata/rename/bad/bad.go.in
rename to internal/lsp/testdata/lsp/primarymod/rename/bad/bad.go.in
diff --git a/internal/lsp/testdata/rename/bad/bad_test.go.in b/internal/lsp/testdata/lsp/primarymod/rename/bad/bad_test.go.in
similarity index 100%
rename from internal/lsp/testdata/rename/bad/bad_test.go.in
rename to internal/lsp/testdata/lsp/primarymod/rename/bad/bad_test.go.in
diff --git a/internal/lsp/testdata/rename/crosspkg/crosspkg.go b/internal/lsp/testdata/lsp/primarymod/rename/crosspkg/crosspkg.go
similarity index 100%
rename from internal/lsp/testdata/rename/crosspkg/crosspkg.go
rename to internal/lsp/testdata/lsp/primarymod/rename/crosspkg/crosspkg.go
diff --git a/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden b/internal/lsp/testdata/lsp/primarymod/rename/crosspkg/crosspkg.go.golden
similarity index 100%
rename from internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden
rename to internal/lsp/testdata/lsp/primarymod/rename/crosspkg/crosspkg.go.golden
diff --git a/internal/lsp/testdata/rename/crosspkg/other/other.go b/internal/lsp/testdata/lsp/primarymod/rename/crosspkg/other/other.go
similarity index 100%
rename from internal/lsp/testdata/rename/crosspkg/other/other.go
rename to internal/lsp/testdata/lsp/primarymod/rename/crosspkg/other/other.go
diff --git a/internal/lsp/testdata/rename/crosspkg/other/other.go.golden b/internal/lsp/testdata/lsp/primarymod/rename/crosspkg/other/other.go.golden
similarity index 100%
rename from internal/lsp/testdata/rename/crosspkg/other/other.go.golden
rename to internal/lsp/testdata/lsp/primarymod/rename/crosspkg/other/other.go.golden
diff --git a/internal/lsp/testdata/rename/testy/testy.go b/internal/lsp/testdata/lsp/primarymod/rename/testy/testy.go
similarity index 100%
rename from internal/lsp/testdata/rename/testy/testy.go
rename to internal/lsp/testdata/lsp/primarymod/rename/testy/testy.go
diff --git a/internal/lsp/testdata/rename/testy/testy.go.golden b/internal/lsp/testdata/lsp/primarymod/rename/testy/testy.go.golden
similarity index 100%
rename from internal/lsp/testdata/rename/testy/testy.go.golden
rename to internal/lsp/testdata/lsp/primarymod/rename/testy/testy.go.golden
diff --git a/internal/lsp/testdata/rename/testy/testy_test.go b/internal/lsp/testdata/lsp/primarymod/rename/testy/testy_test.go
similarity index 100%
rename from internal/lsp/testdata/rename/testy/testy_test.go
rename to internal/lsp/testdata/lsp/primarymod/rename/testy/testy_test.go
diff --git a/internal/lsp/testdata/rename/testy/testy_test.go.golden b/internal/lsp/testdata/lsp/primarymod/rename/testy/testy_test.go.golden
similarity index 100%
rename from internal/lsp/testdata/rename/testy/testy_test.go.golden
rename to internal/lsp/testdata/lsp/primarymod/rename/testy/testy_test.go.golden
diff --git a/internal/lsp/testdata/selector/selector.go.in b/internal/lsp/testdata/lsp/primarymod/selector/selector.go.in
similarity index 100%
rename from internal/lsp/testdata/selector/selector.go.in
rename to internal/lsp/testdata/lsp/primarymod/selector/selector.go.in
diff --git a/internal/lsp/testdata/signature/signature.go b/internal/lsp/testdata/lsp/primarymod/signature/signature.go
similarity index 100%
rename from internal/lsp/testdata/signature/signature.go
rename to internal/lsp/testdata/lsp/primarymod/signature/signature.go
diff --git a/internal/lsp/testdata/signature/signature.go.golden b/internal/lsp/testdata/lsp/primarymod/signature/signature.go.golden
similarity index 97%
rename from internal/lsp/testdata/signature/signature.go.golden
rename to internal/lsp/testdata/lsp/primarymod/signature/signature.go.golden
index dafd426..22d2a51 100644
--- a/internal/lsp/testdata/signature/signature.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/signature/signature.go.golden
@@ -1,5 +1,3 @@
--- -signature --
-
-- Bar(float64, ...byte)-signature --
Bar(float64, ...byte)
diff --git a/internal/lsp/testdata/signature/signature2.go.golden b/internal/lsp/testdata/lsp/primarymod/signature/signature2.go.golden
similarity index 100%
rename from internal/lsp/testdata/signature/signature2.go.golden
rename to internal/lsp/testdata/lsp/primarymod/signature/signature2.go.golden
diff --git a/internal/lsp/testdata/signature/signature2.go.in b/internal/lsp/testdata/lsp/primarymod/signature/signature2.go.in
similarity index 100%
rename from internal/lsp/testdata/signature/signature2.go.in
rename to internal/lsp/testdata/lsp/primarymod/signature/signature2.go.in
diff --git a/internal/lsp/testdata/snippets/literal_snippets.go.in b/internal/lsp/testdata/lsp/primarymod/snippets/literal_snippets.go.in
similarity index 96%
rename from internal/lsp/testdata/snippets/literal_snippets.go.in
rename to internal/lsp/testdata/lsp/primarymod/snippets/literal_snippets.go.in
index 9906a2b..ffaa125 100644
--- a/internal/lsp/testdata/snippets/literal_snippets.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/snippets/literal_snippets.go.in
@@ -180,3 +180,15 @@
var mi myInt
mi = my //@snippet(" //", litMyInt, "myInt($0)", "myInt($0)")
}
+
+func _() {
+ type ptrStruct struct {
+ p *ptrStruct
+ }
+
+ ptrStruct{} //@item(litPtrStruct, "ptrStruct{}", "", "var")
+
+ ptrStruct{
+ p: &ptrSt, //@rank(",", litPtrStruct)
+ }
+}
diff --git a/internal/lsp/testdata/snippets/snippets.go.golden b/internal/lsp/testdata/lsp/primarymod/snippets/snippets.go.golden
similarity index 79%
rename from internal/lsp/testdata/snippets/snippets.go.golden
rename to internal/lsp/testdata/lsp/primarymod/snippets/snippets.go.golden
index 34b919e..3f20ba5 100644
--- a/internal/lsp/testdata/snippets/snippets.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/snippets/snippets.go.golden
@@ -1,5 +1,3 @@
--- -signature --
-
-- baz(at AliasType, b bool)-signature --
baz(at AliasType, b bool)
diff --git a/internal/lsp/testdata/snippets/snippets.go.in b/internal/lsp/testdata/lsp/primarymod/snippets/snippets.go.in
similarity index 100%
rename from internal/lsp/testdata/snippets/snippets.go.in
rename to internal/lsp/testdata/lsp/primarymod/snippets/snippets.go.in
diff --git a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go b/internal/lsp/testdata/lsp/primarymod/suggestedfix/has_suggested_fix.go
similarity index 87%
rename from internal/lsp/testdata/suggestedfix/has_suggested_fix.go
rename to internal/lsp/testdata/lsp/primarymod/suggestedfix/has_suggested_fix.go
index 9ade674..ccd198c 100644
--- a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go
+++ b/internal/lsp/testdata/lsp/primarymod/suggestedfix/has_suggested_fix.go
@@ -7,5 +7,5 @@
func goodbye() {
s := "hiiiiiii"
s = s //@suggestedfix("s = s")
- log.Printf(s)
+ log.Print(s)
}
diff --git a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden b/internal/lsp/testdata/lsp/primarymod/suggestedfix/has_suggested_fix.go.golden
similarity index 65%
rename from internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden
rename to internal/lsp/testdata/lsp/primarymod/suggestedfix/has_suggested_fix.go.golden
index 10ec450..4923ecc 100644
--- a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/suggestedfix/has_suggested_fix.go.golden
@@ -1,4 +1,4 @@
--- suggestedfix --
+-- suggestedfix_has_suggested_fix_9_2 --
package suggestedfix
import (
@@ -8,6 +8,6 @@
func goodbye() {
s := "hiiiiiii"
//@suggestedfix("s = s")
- log.Printf(s)
+ log.Print(s)
}
diff --git a/internal/lsp/testdata/symbols/main.go b/internal/lsp/testdata/lsp/primarymod/symbols/main.go
similarity index 61%
rename from internal/lsp/testdata/symbols/main.go
rename to internal/lsp/testdata/lsp/primarymod/symbols/main.go
index b9ee77c..0e8d5b6 100644
--- a/internal/lsp/testdata/symbols/main.go
+++ b/internal/lsp/testdata/lsp/primarymod/symbols/main.go
@@ -4,7 +4,7 @@
"io"
)
-var x = 42 //@symbol("x", "x", "Variable", "")
+var x = 42 //@mark(symbolsx, "x"), symbol("x", "x", "Variable", "")
const y = 43 //@symbol("y", "y", "Constant", "")
@@ -19,22 +19,22 @@
BoolAlias = bool //@symbol("BoolAlias", "BoolAlias", "Boolean", "")
)
-type Foo struct { //@symbol("Foo", "Foo", "Struct", "")
- Quux //@symbol("Quux", "Quux", "Field", "Foo")
+type Foo struct { //@mark(symbolsFoo, "Foo"), symbol("Foo", "Foo", "Struct", "")
+ Quux //@mark(fQuux, "Quux"), symbol("Quux", "Quux", "Field", "Foo")
W io.Writer //@symbol("W" , "W", "Field", "Foo")
- Bar int //@symbol("Bar", "Bar", "Field", "Foo")
+ Bar int //@mark(fBar, "Bar"), symbol("Bar", "Bar", "Field", "Foo")
baz string //@symbol("baz", "baz", "Field", "Foo")
}
type Quux struct { //@symbol("Quux", "Quux", "Struct", "")
- X, Y float64 //@symbol("X", "X", "Field", "Quux"), symbol("Y", "Y", "Field", "Quux")
+ X, Y float64 //@mark(qX, "X"), symbol("X", "X", "Field", "Quux"), symbol("Y", "Y", "Field", "Quux")
}
func (f Foo) Baz() string { //@symbol("Baz", "Baz", "Method", "Foo")
return f.baz
}
-func (q *Quux) Do() {} //@symbol("Do", "Do", "Method", "Quux")
+func (q *Quux) Do() {} //@mark(qDo, "Do"), symbol("Do", "Do", "Method", "Quux")
func main() { //@symbol("main", "main", "Function", "")
@@ -44,13 +44,13 @@
String() string //@symbol("String", "String", "Method", "Stringer")
}
-type ABer interface { //@symbol("ABer", "ABer", "Interface", "")
+type ABer interface { //@mark(ABerInterface, "ABer"), symbol("ABer", "ABer", "Interface", "")
B() //@symbol("B", "B", "Method", "ABer")
- A() string //@symbol("A", "A", "Method", "ABer")
+ A() string //@mark(ABerA, "A"), symbol("A", "A", "Method", "ABer")
}
type WithEmbeddeds interface { //@symbol("WithEmbeddeds", "WithEmbeddeds", "Interface", "")
Do() //@symbol("Do", "Do", "Method", "WithEmbeddeds")
ABer //@symbol("ABer", "ABer", "Interface", "WithEmbeddeds")
- io.Writer //@symbol("io.Writer", "io.Writer", "Interface", "WithEmbeddeds")
+ io.Writer //@mark(ioWriter, "io.Writer"), symbol("io.Writer", "io.Writer", "Interface", "WithEmbeddeds")
}
diff --git a/internal/lsp/testdata/symbols/main.go.golden b/internal/lsp/testdata/lsp/primarymod/symbols/main.go.golden
similarity index 100%
rename from internal/lsp/testdata/symbols/main.go.golden
rename to internal/lsp/testdata/lsp/primarymod/symbols/main.go.golden
diff --git a/internal/lsp/testdata/testy/testy.go b/internal/lsp/testdata/lsp/primarymod/testy/testy.go
similarity index 100%
rename from internal/lsp/testdata/testy/testy.go
rename to internal/lsp/testdata/lsp/primarymod/testy/testy.go
diff --git a/internal/lsp/testdata/testy/testy_test.go b/internal/lsp/testdata/lsp/primarymod/testy/testy_test.go
similarity index 83%
rename from internal/lsp/testdata/testy/testy_test.go
rename to internal/lsp/testdata/lsp/primarymod/testy/testy_test.go
index 4bc6207..828a494 100644
--- a/internal/lsp/testdata/testy/testy_test.go
+++ b/internal/lsp/testdata/lsp/primarymod/testy/testy_test.go
@@ -3,6 +3,6 @@
import "testing"
func TestSomething(t *testing.T) { //@item(TestSomething, "TestSomething(t *testing.T)", "", "func")
- var x int //@mark(testyX, "x"),diag("x", "compiler", "x declared but not used"),refs("x", testyX)
+ var x int //@mark(testyX, "x"),diag("x", "compiler", "x declared but not used", "error"),refs("x", testyX)
a() //@mark(testyA, "a")
}
diff --git a/internal/lsp/testdata/typeassert/type_assert.go b/internal/lsp/testdata/lsp/primarymod/typeassert/type_assert.go
similarity index 100%
rename from internal/lsp/testdata/typeassert/type_assert.go
rename to internal/lsp/testdata/lsp/primarymod/typeassert/type_assert.go
diff --git a/internal/lsp/testdata/types/types.go b/internal/lsp/testdata/lsp/primarymod/types/types.go
similarity index 100%
rename from internal/lsp/testdata/types/types.go
rename to internal/lsp/testdata/lsp/primarymod/types/types.go
diff --git a/internal/lsp/testdata/unimported/export_test.go b/internal/lsp/testdata/lsp/primarymod/unimported/export_test.go
similarity index 100%
rename from internal/lsp/testdata/unimported/export_test.go
rename to internal/lsp/testdata/lsp/primarymod/unimported/export_test.go
diff --git a/internal/lsp/testdata/lsp/primarymod/unimported/unimported.go.in b/internal/lsp/testdata/lsp/primarymod/unimported/unimported.go.in
new file mode 100644
index 0000000..d9f109c
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/unimported/unimported.go.in
@@ -0,0 +1,22 @@
+package unimported
+
+func _() {
+ http //@unimported("p", nethttp, nethttptest)
+ pkg //@unimported("g", externalpackage)
+ // container/ring is extremely unlikely to be imported by anything, so shouldn't have type information.
+ ring.Ring //@unimported("Ring", ringring)
+ signature.Foo //@unimported("Foo", signaturefoo)
+ context.Bac //@unimported("Bac", contextBackground, contextBackgroundErr)
+}
+
+// Create markers for unimported std lib packages. Only for use by this test.
+/* http */ //@item(nethttp, "http", "\"net/http\"", "package")
+/* httptest */ //@item(nethttptest, "httptest", "\"net/http/httptest\"", "package")
+/* pkg */ //@item(externalpackage, "pkg", "\"example.com/extramodule/pkg\"", "package")
+
+/* ring.Ring */ //@item(ringring, "Ring", "(from \"container/ring\")", "var")
+
+/* signature.Foo */ //@item(signaturefoo, "Foo", "func(a string, b int) (c bool) (from \"golang.org/x/tools/internal/lsp/signature\")", "func")
+
+/* context.Background */ //@item(contextBackground, "Background", "func() context.Context (from \"context\")", "func")
+/* context.Background().Err */ //@item(contextBackgroundErr, "Background().Err", "func() error (from \"context\")", "method")
diff --git a/internal/lsp/testdata/unimported/unimported_cand_type.go b/internal/lsp/testdata/lsp/primarymod/unimported/unimported_cand_type.go
similarity index 73%
rename from internal/lsp/testdata/unimported/unimported_cand_type.go
rename to internal/lsp/testdata/lsp/primarymod/unimported/unimported_cand_type.go
index 2690c1a..531aa2d 100644
--- a/internal/lsp/testdata/unimported/unimported_cand_type.go
+++ b/internal/lsp/testdata/lsp/primarymod/unimported/unimported_cand_type.go
@@ -1,8 +1,10 @@
package unimported
import (
+ _ "context"
+
"golang.org/x/tools/internal/lsp/baz"
- "golang.org/x/tools/internal/lsp/signature" // provide type information for unimported completions in the other file
+ _ "golang.org/x/tools/internal/lsp/signature" // provide type information for unimported completions in the other file
)
func _() {
diff --git a/internal/lsp/testdata/unimported/x_test.go b/internal/lsp/testdata/lsp/primarymod/unimported/x_test.go
similarity index 100%
rename from internal/lsp/testdata/unimported/x_test.go
rename to internal/lsp/testdata/lsp/primarymod/unimported/x_test.go
diff --git a/internal/lsp/testdata/lsp/primarymod/unresolved/unresolved.go.in b/internal/lsp/testdata/lsp/primarymod/unresolved/unresolved.go.in
new file mode 100644
index 0000000..e1daecc
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/unresolved/unresolved.go.in
@@ -0,0 +1,6 @@
+package unresolved
+
+func foo(interface{}) {
+ // don't crash on fake "resolved" type
+ foo(func(i, j f //@complete(" //")
+}
diff --git a/internal/lsp/testdata/unsafe/unsafe.go b/internal/lsp/testdata/lsp/primarymod/unsafe/unsafe.go
similarity index 100%
rename from internal/lsp/testdata/unsafe/unsafe.go
rename to internal/lsp/testdata/lsp/primarymod/unsafe/unsafe.go
diff --git a/internal/lsp/testdata/variadic/variadic.go.in b/internal/lsp/testdata/lsp/primarymod/variadic/variadic.go.in
similarity index 83%
rename from internal/lsp/testdata/variadic/variadic.go.in
rename to internal/lsp/testdata/lsp/primarymod/variadic/variadic.go.in
index 8a7fa24..f715719 100644
--- a/internal/lsp/testdata/variadic/variadic.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/variadic/variadic.go.in
@@ -21,3 +21,10 @@
// snippet will add the "..." for you
foo(123, ) //@snippet(")", vStrSlice, "ss...", "ss..."),snippet(")", vFunc, "bar()...", "bar()..."),snippet(")", vStr, "s", "s")
}
+
+func qux(...func()) {}
+func f() {} //@item(vVarArg, "f", "func()", "func")
+
+func _() {
+ qux(f) //@snippet(")", vVarArg, "f", "f")
+}
diff --git a/internal/lsp/testdata/variadic/variadic_intf.go b/internal/lsp/testdata/lsp/primarymod/variadic/variadic_intf.go
similarity index 100%
rename from internal/lsp/testdata/variadic/variadic_intf.go
rename to internal/lsp/testdata/lsp/primarymod/variadic/variadic_intf.go
diff --git a/internal/lsp/testdata/lsp/primarymod/workspacesymbol/a/a.go b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/a/a.go
new file mode 100644
index 0000000..040b49e
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/a/a.go
@@ -0,0 +1,9 @@
+package a
+
+var WorkspaceSymbolVariableA = "a" //@symbol("WorkspaceSymbolVariableA", "WorkspaceSymbolVariableA", "Variable", "")
+
+const WorkspaceSymbolConstantA = "a" //@symbol("WorkspaceSymbolConstantA", "WorkspaceSymbolConstantA", "Constant", "")
+
+const (
+ workspacesymbolinvariable = iota //@symbol("workspacesymbolinvariable", "workspacesymbolinvariable", "Constant", "")
+)
diff --git a/internal/lsp/testdata/lsp/primarymod/workspacesymbol/a/a.go.golden b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/a/a.go.golden
new file mode 100644
index 0000000..2a8788b
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/a/a.go.golden
@@ -0,0 +1,5 @@
+-- symbols --
+WorkspaceSymbolVariableA Variable 3:5-3:29
+WorkspaceSymbolConstantA Constant 5:7-5:31
+workspacesymbolinvariable Constant 8:2-8:27
+
diff --git a/internal/lsp/testdata/lsp/primarymod/workspacesymbol/b/b.go b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/b/b.go
new file mode 100644
index 0000000..d7469af
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/b/b.go
@@ -0,0 +1,7 @@
+package b
+
+var WorkspaceSymbolVariableB = "b" //@symbol("WorkspaceSymbolVariableB", "WorkspaceSymbolVariableB", "Variable", "")
+
+type WorkspaceSymbolStructB struct { //@symbol("WorkspaceSymbolStructB", "WorkspaceSymbolStructB", "Struct", "")
+ Bar int //@mark(bBar, "Bar"), symbol("Bar", "Bar", "Field", "WorkspaceSymbolStructB")
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/workspacesymbol/b/b.go.golden b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/b/b.go.golden
new file mode 100644
index 0000000..ecc8781
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/b/b.go.golden
@@ -0,0 +1,5 @@
+-- symbols --
+WorkspaceSymbolVariableB Variable 3:5-3:29
+WorkspaceSymbolStructB Struct 5:6-5:28
+ Bar Field 6:2-6:5
+
diff --git a/internal/lsp/testdata/lsp/primarymod/workspacesymbol/casesensitive/casesensitive.go b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/casesensitive/casesensitive.go
new file mode 100644
index 0000000..9b60651
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/casesensitive/casesensitive.go
@@ -0,0 +1,6 @@
+package casesensitive
+
+/*@
+workspacesymbolcasesensitive("baz", baz)
+workspacesymbolcasesensitive("Baz", Baz)
+*/
diff --git a/internal/lsp/testdata/lsp/primarymod/workspacesymbol/fuzzy/fuzzy.go b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/fuzzy/fuzzy.go
new file mode 100644
index 0000000..de03d36
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/workspacesymbol/fuzzy/fuzzy.go
@@ -0,0 +1,23 @@
+package fuzzy
+
+/*@
+workspacesymbolfuzzy("wsym",
+ WorkspaceSymbolVariableA,
+ WorkspaceSymbolConstantA,
+ workspacesymbolinvariable,
+ WorkspaceSymbolVariableB,
+ WorkspaceSymbolStructB,
+)
+workspacesymbolfuzzy("symbola",
+ WorkspaceSymbolVariableA,
+ WorkspaceSymbolConstantA,
+ workspacesymbolinvariable,
+ WorkspaceSymbolVariableB,
+)
+workspacesymbolfuzzy("symbolb",
+ WorkspaceSymbolVariableA,
+ workspacesymbolinvariable,
+ WorkspaceSymbolVariableB,
+ WorkspaceSymbolStructB,
+)
+*/
diff --git a/internal/lsp/testdata/lsp/summary.txt.golden b/internal/lsp/testdata/lsp/summary.txt.golden
new file mode 100644
index 0000000..f1ef410
--- /dev/null
+++ b/internal/lsp/testdata/lsp/summary.txt.golden
@@ -0,0 +1,28 @@
+-- summary --
+CodeLensCount = 0
+CompletionsCount = 226
+CompletionSnippetCount = 68
+UnimportedCompletionsCount = 11
+DeepCompletionsCount = 5
+FuzzyCompletionsCount = 8
+RankedCompletionsCount = 111
+CaseSensitiveCompletionsCount = 4
+DiagnosticsCount = 39
+FoldingRangesCount = 2
+FormatCount = 6
+ImportCount = 7
+SuggestedFixCount = 1
+DefinitionsCount = 45
+TypeDefinitionsCount = 2
+HighlightsCount = 52
+ReferencesCount = 9
+RenamesCount = 23
+PrepareRenamesCount = 7
+SymbolsCount = 3
+WorkspaceSymbolsCount = 0
+FuzzyWorkspaceSymbolsCount = 3
+CaseSensitiveWorkspaceSymbolsCount = 2
+SignaturesCount = 23
+LinksCount = 8
+ImplementationsCount = 5
+
diff --git a/internal/lsp/testdata/missingdep/modules/example.com/extramodule/pkg/x.go b/internal/lsp/testdata/missingdep/modules/example.com/extramodule/pkg/x.go
new file mode 100644
index 0000000..cf7fc67
--- /dev/null
+++ b/internal/lsp/testdata/missingdep/modules/example.com/extramodule/pkg/x.go
@@ -0,0 +1,3 @@
+package pkg
+
+const Test = 1
\ No newline at end of file
diff --git a/internal/lsp/testdata/missingdep/primarymod/go.mod b/internal/lsp/testdata/missingdep/primarymod/go.mod
new file mode 100644
index 0000000..3c64aee
--- /dev/null
+++ b/internal/lsp/testdata/missingdep/primarymod/go.mod
@@ -0,0 +1,3 @@
+module missingdep
+
+go 1.12
diff --git a/internal/lsp/testdata/missingdep/primarymod/go.mod.golden b/internal/lsp/testdata/missingdep/primarymod/go.mod.golden
new file mode 100644
index 0000000..862c051
--- /dev/null
+++ b/internal/lsp/testdata/missingdep/primarymod/go.mod.golden
@@ -0,0 +1,7 @@
+-- suggestedfix_main_5_2 --
+module missingdep
+
+go 1.12
+
+require example.com/extramodule v1.0.0
+
diff --git a/internal/lsp/testdata/missingdep/primarymod/main.go b/internal/lsp/testdata/missingdep/primarymod/main.go
new file mode 100644
index 0000000..b22d1fd
--- /dev/null
+++ b/internal/lsp/testdata/missingdep/primarymod/main.go
@@ -0,0 +1,10 @@
+// Package missingdep does something
+package missingdep
+
+import (
+ "example.com/extramodule/pkg" //@diag("\"example.com/extramodule/pkg\"", "go mod tidy", "example.com/extramodule is not in your go.mod file.", "warning"),suggestedfix("\"example.com/extramodule/pkg\"")
+)
+
+func Yo() {
+ _ = pkg.Test
+}
diff --git a/internal/lsp/testdata/missingdep/summary.txt.golden b/internal/lsp/testdata/missingdep/summary.txt.golden
new file mode 100644
index 0000000..5c4f74a
--- /dev/null
+++ b/internal/lsp/testdata/missingdep/summary.txt.golden
@@ -0,0 +1,28 @@
+-- summary --
+CodeLensCount = 0
+CompletionsCount = 0
+CompletionSnippetCount = 0
+UnimportedCompletionsCount = 0
+DeepCompletionsCount = 0
+FuzzyCompletionsCount = 0
+RankedCompletionsCount = 0
+CaseSensitiveCompletionsCount = 0
+DiagnosticsCount = 1
+FoldingRangesCount = 0
+FormatCount = 0
+ImportCount = 0
+SuggestedFixCount = 1
+DefinitionsCount = 0
+TypeDefinitionsCount = 0
+HighlightsCount = 0
+ReferencesCount = 0
+RenamesCount = 0
+PrepareRenamesCount = 0
+SymbolsCount = 0
+WorkspaceSymbolsCount = 0
+FuzzyWorkspaceSymbolsCount = 0
+CaseSensitiveWorkspaceSymbolsCount = 0
+SignaturesCount = 0
+LinksCount = 0
+ImplementationsCount = 0
+
diff --git a/internal/lsp/testdata/missingtwodep/modules/example.com/anothermodule/hey/y.go b/internal/lsp/testdata/missingtwodep/modules/example.com/anothermodule/hey/y.go
new file mode 100644
index 0000000..1957ebb
--- /dev/null
+++ b/internal/lsp/testdata/missingtwodep/modules/example.com/anothermodule/hey/y.go
@@ -0,0 +1,3 @@
+package hey
+
+const Test = 1
\ No newline at end of file
diff --git a/internal/lsp/testdata/missingtwodep/modules/example.com/extramodule/pkg/x.go b/internal/lsp/testdata/missingtwodep/modules/example.com/extramodule/pkg/x.go
new file mode 100644
index 0000000..cf7fc67
--- /dev/null
+++ b/internal/lsp/testdata/missingtwodep/modules/example.com/extramodule/pkg/x.go
@@ -0,0 +1,3 @@
+package pkg
+
+const Test = 1
\ No newline at end of file
diff --git a/internal/lsp/testdata/missingtwodep/modules/example.com/extramodule/yo/y.go b/internal/lsp/testdata/missingtwodep/modules/example.com/extramodule/yo/y.go
new file mode 100644
index 0000000..f825aa5
--- /dev/null
+++ b/internal/lsp/testdata/missingtwodep/modules/example.com/extramodule/yo/y.go
@@ -0,0 +1,3 @@
+package yo
+
+const Test = 1
\ No newline at end of file
diff --git a/internal/lsp/testdata/missingtwodep/primarymod/go.mod b/internal/lsp/testdata/missingtwodep/primarymod/go.mod
new file mode 100644
index 0000000..eba8c43
--- /dev/null
+++ b/internal/lsp/testdata/missingtwodep/primarymod/go.mod
@@ -0,0 +1,3 @@
+module missingtwodep
+
+go 1.12
diff --git a/internal/lsp/testdata/missingtwodep/primarymod/go.mod.golden b/internal/lsp/testdata/missingtwodep/primarymod/go.mod.golden
new file mode 100644
index 0000000..6be2a4a
--- /dev/null
+++ b/internal/lsp/testdata/missingtwodep/primarymod/go.mod.golden
@@ -0,0 +1,21 @@
+-- suggestedfix_main_5_2 --
+module missingtwodep
+
+go 1.12
+
+require example.com/anothermodule v1.0.0
+
+-- suggestedfix_main_6_2 --
+module missingtwodep
+
+go 1.12
+
+require example.com/extramodule v1.0.0
+
+-- suggestedfix_main_7_2 --
+module missingtwodep
+
+go 1.12
+
+require example.com/extramodule v1.0.0
+
diff --git a/internal/lsp/testdata/missingtwodep/primarymod/main.go b/internal/lsp/testdata/missingtwodep/primarymod/main.go
new file mode 100644
index 0000000..b6cdcdc
--- /dev/null
+++ b/internal/lsp/testdata/missingtwodep/primarymod/main.go
@@ -0,0 +1,14 @@
+// Package missingtwodep does something
+package missingtwodep
+
+import (
+ "example.com/anothermodule/hey" //@diag("\"example.com/anothermodule/hey\"", "go mod tidy", "example.com/anothermodule is not in your go.mod file.", "warning"),suggestedfix("\"example.com/anothermodule/hey\"")
+ "example.com/extramodule/pkg" //@diag("\"example.com/extramodule/pkg\"", "go mod tidy", "example.com/extramodule is not in your go.mod file.", "warning"),suggestedfix("\"example.com/extramodule/pkg\"")
+ "example.com/extramodule/yo" //@diag("\"example.com/extramodule/yo\"", "go mod tidy", "example.com/extramodule is not in your go.mod file.", "warning"),suggestedfix("\"example.com/extramodule/yo\"")
+)
+
+func Yo() {
+ _ = pkg.Test
+ _ = yo.Test
+ _ = hey.Test
+}
diff --git a/internal/lsp/testdata/missingtwodep/summary.txt.golden b/internal/lsp/testdata/missingtwodep/summary.txt.golden
new file mode 100644
index 0000000..96ac475
--- /dev/null
+++ b/internal/lsp/testdata/missingtwodep/summary.txt.golden
@@ -0,0 +1,28 @@
+-- summary --
+CodeLensCount = 0
+CompletionsCount = 0
+CompletionSnippetCount = 0
+UnimportedCompletionsCount = 0
+DeepCompletionsCount = 0
+FuzzyCompletionsCount = 0
+RankedCompletionsCount = 0
+CaseSensitiveCompletionsCount = 0
+DiagnosticsCount = 3
+FoldingRangesCount = 0
+FormatCount = 0
+ImportCount = 0
+SuggestedFixCount = 3
+DefinitionsCount = 0
+TypeDefinitionsCount = 0
+HighlightsCount = 0
+ReferencesCount = 0
+RenamesCount = 0
+PrepareRenamesCount = 0
+SymbolsCount = 0
+WorkspaceSymbolsCount = 0
+FuzzyWorkspaceSymbolsCount = 0
+CaseSensitiveWorkspaceSymbolsCount = 0
+SignaturesCount = 0
+LinksCount = 0
+ImplementationsCount = 0
+
diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden
deleted file mode 100644
index 9efddc4..0000000
--- a/internal/lsp/testdata/summary.txt.golden
+++ /dev/null
@@ -1,24 +0,0 @@
--- summary --
-CompletionsCount = 226
-CompletionSnippetCount = 66
-UnimportedCompletionsCount = 9
-DeepCompletionsCount = 5
-FuzzyCompletionsCount = 8
-RankedCompletionsCount = 67
-CaseSensitiveCompletionsCount = 4
-DiagnosticsCount = 38
-FoldingRangesCount = 2
-FormatCount = 6
-ImportCount = 7
-SuggestedFixCount = 1
-DefinitionsCount = 43
-TypeDefinitionsCount = 2
-HighlightsCount = 52
-ReferencesCount = 9
-RenamesCount = 23
-PrepareRenamesCount = 8
-SymbolsCount = 1
-SignaturesCount = 23
-LinksCount = 8
-ImplementationsCount = 5
-
diff --git a/internal/lsp/testdata/unimported/unimported.go.in b/internal/lsp/testdata/unimported/unimported.go.in
deleted file mode 100644
index d4ce8b3..0000000
--- a/internal/lsp/testdata/unimported/unimported.go.in
+++ /dev/null
@@ -1,20 +0,0 @@
-package unimported
-
-func _() {
- //@unimported("", hashslashadler32, goslashast, encodingslashbase64, bytes)
- pkg //@unimported("g", externalpackage)
- // container/ring is extremely unlikely to be imported by anything, so shouldn't have type information.
- ring.Ring //@unimported("Ring", ringring)
- signature.Foo //@unimported("Foo", signaturefoo)
-}
-
-// Create markers for unimported std lib packages. Only for use by this test.
-/* adler32 */ //@item(hashslashadler32, "adler32", "\"hash/adler32\"", "package")
-/* ast */ //@item(goslashast, "ast", "\"go/ast\"", "package")
-/* base64 */ //@item(encodingslashbase64, "base64", "\"encoding/base64\"", "package")
-/* bytes */ //@item(bytes, "bytes", "\"bytes\"", "package")
-/* pkg */ //@item(externalpackage, "pkg", "\"example.com/extramodule/pkg\"", "package")
-
-/* ring.Ring */ //@item(ringring, "Ring", "(from \"container/ring\")", "var")
-
-/* signature.Foo */ //@item(signaturefoo, "Foo", "func(a string, b int) (c bool) (from \"golang.org/x/tools/internal/lsp/signature\")", "func")
\ No newline at end of file
diff --git a/internal/lsp/testdata/unresolved/unresolved.go.in b/internal/lsp/testdata/unresolved/unresolved.go.in
deleted file mode 100644
index ceb7fe2..0000000
--- a/internal/lsp/testdata/unresolved/unresolved.go.in
+++ /dev/null
@@ -1,6 +0,0 @@
-package unresolved
-
-func foo(interface{}) { //@item(unresolvedFoo, "foo", "func(interface{})", "func")
- // don't crash on fake "resolved" type
- foo(func(i, j f //@complete(" //", unresolvedFoo)
-}
diff --git a/internal/lsp/testdata/unused/modules/example.com/extramodule/pkg/x.go b/internal/lsp/testdata/unused/modules/example.com/extramodule/pkg/x.go
new file mode 100644
index 0000000..cf7fc67
--- /dev/null
+++ b/internal/lsp/testdata/unused/modules/example.com/extramodule/pkg/x.go
@@ -0,0 +1,3 @@
+package pkg
+
+const Test = 1
\ No newline at end of file
diff --git a/internal/lsp/testdata/unused/primarymod/go.mod b/internal/lsp/testdata/unused/primarymod/go.mod
new file mode 100644
index 0000000..6ff23f0
--- /dev/null
+++ b/internal/lsp/testdata/unused/primarymod/go.mod
@@ -0,0 +1,5 @@
+module unused
+
+go 1.12
+
+require example.com/extramodule v1.0.0 //@diag("require example.com/extramodule v1.0.0", "go mod tidy", "example.com/extramodule is not used in this module.", "warning"),suggestedfix("require example.com/extramodule v1.0.0")
diff --git a/internal/lsp/testdata/unused/primarymod/go.mod.golden b/internal/lsp/testdata/unused/primarymod/go.mod.golden
new file mode 100644
index 0000000..d97d794
--- /dev/null
+++ b/internal/lsp/testdata/unused/primarymod/go.mod.golden
@@ -0,0 +1,5 @@
+-- suggestedfix_go.mod_5_1 --
+module unused
+
+go 1.12
+
diff --git a/internal/lsp/mod/testdata/unused/main.go b/internal/lsp/testdata/unused/primarymod/main.go
similarity index 100%
rename from internal/lsp/mod/testdata/unused/main.go
rename to internal/lsp/testdata/unused/primarymod/main.go
diff --git a/internal/lsp/testdata/unused/summary.txt.golden b/internal/lsp/testdata/unused/summary.txt.golden
new file mode 100644
index 0000000..5c4f74a
--- /dev/null
+++ b/internal/lsp/testdata/unused/summary.txt.golden
@@ -0,0 +1,28 @@
+-- summary --
+CodeLensCount = 0
+CompletionsCount = 0
+CompletionSnippetCount = 0
+UnimportedCompletionsCount = 0
+DeepCompletionsCount = 0
+FuzzyCompletionsCount = 0
+RankedCompletionsCount = 0
+CaseSensitiveCompletionsCount = 0
+DiagnosticsCount = 1
+FoldingRangesCount = 0
+FormatCount = 0
+ImportCount = 0
+SuggestedFixCount = 1
+DefinitionsCount = 0
+TypeDefinitionsCount = 0
+HighlightsCount = 0
+ReferencesCount = 0
+RenamesCount = 0
+PrepareRenamesCount = 0
+SymbolsCount = 0
+WorkspaceSymbolsCount = 0
+FuzzyWorkspaceSymbolsCount = 0
+CaseSensitiveWorkspaceSymbolsCount = 0
+SignaturesCount = 0
+LinksCount = 0
+ImplementationsCount = 0
+
diff --git a/internal/lsp/testdata/upgradedep/modules/example.com/extramodule@v1.0.0/pkg/x.go b/internal/lsp/testdata/upgradedep/modules/example.com/extramodule@v1.0.0/pkg/x.go
new file mode 100644
index 0000000..cf7fc67
--- /dev/null
+++ b/internal/lsp/testdata/upgradedep/modules/example.com/extramodule@v1.0.0/pkg/x.go
@@ -0,0 +1,3 @@
+package pkg
+
+const Test = 1
\ No newline at end of file
diff --git a/internal/lsp/testdata/upgradedep/modules/example.com/extramodule@v1.1.0/pkg/x.go b/internal/lsp/testdata/upgradedep/modules/example.com/extramodule@v1.1.0/pkg/x.go
new file mode 100644
index 0000000..cf7fc67
--- /dev/null
+++ b/internal/lsp/testdata/upgradedep/modules/example.com/extramodule@v1.1.0/pkg/x.go
@@ -0,0 +1,3 @@
+package pkg
+
+const Test = 1
\ No newline at end of file
diff --git a/internal/lsp/testdata/upgradedep/primarymod/go.mod b/internal/lsp/testdata/upgradedep/primarymod/go.mod
new file mode 100644
index 0000000..ac1ed82
--- /dev/null
+++ b/internal/lsp/testdata/upgradedep/primarymod/go.mod
@@ -0,0 +1,11 @@
+module upgradedep
+
+// TODO(microsoft/vscode-go#12): Another issue. //@link(`microsoft/vscode-go#12`, `https://github.com/microsoft/vscode-go/issues/12`)
+
+go 1.12
+
+// TODO(golang/go#1234): Link the relevant issue. //@link(`golang/go#1234`, `https://github.com/golang/go/issues/1234`)
+
+require example.com/extramodule v1.0.0 //@link(`example.com/extramodule`, `https://pkg.go.dev/mod/example.com/extramodule@v1.0.0`),codelens("require example.com/extramodule v1.0.0", "Upgrade dependency to v1.1.0", "upgrade.dependency")
+
+// https://example.com/comment: Another issue. //@link(`https://example.com/comment`,`https://example.com/comment`)
diff --git a/internal/lsp/testdata/upgradedep/primarymod/main.go b/internal/lsp/testdata/upgradedep/primarymod/main.go
new file mode 100644
index 0000000..467cbf3
--- /dev/null
+++ b/internal/lsp/testdata/upgradedep/primarymod/main.go
@@ -0,0 +1,10 @@
+// Package upgradedep does something
+package upgradedep
+
+import (
+ "example.com/extramodule/pkg"
+)
+
+func Yo() {
+ _ = pkg.Test
+}
diff --git a/internal/lsp/testdata/upgradedep/summary.txt.golden b/internal/lsp/testdata/upgradedep/summary.txt.golden
new file mode 100644
index 0000000..7ae33eb
--- /dev/null
+++ b/internal/lsp/testdata/upgradedep/summary.txt.golden
@@ -0,0 +1,28 @@
+-- summary --
+CodeLensCount = 1
+CompletionsCount = 0
+CompletionSnippetCount = 0
+UnimportedCompletionsCount = 0
+DeepCompletionsCount = 0
+FuzzyCompletionsCount = 0
+RankedCompletionsCount = 0
+CaseSensitiveCompletionsCount = 0
+DiagnosticsCount = 0
+FoldingRangesCount = 0
+FormatCount = 0
+ImportCount = 0
+SuggestedFixCount = 0
+DefinitionsCount = 0
+TypeDefinitionsCount = 0
+HighlightsCount = 0
+ReferencesCount = 0
+RenamesCount = 0
+PrepareRenamesCount = 0
+SymbolsCount = 0
+WorkspaceSymbolsCount = 0
+FuzzyWorkspaceSymbolsCount = 0
+CaseSensitiveWorkspaceSymbolsCount = 0
+SignaturesCount = 0
+LinksCount = 4
+ImplementationsCount = 0
+
diff --git a/internal/lsp/tests/completion.go b/internal/lsp/tests/completion.go
deleted file mode 100644
index ba5ec70..0000000
--- a/internal/lsp/tests/completion.go
+++ /dev/null
@@ -1,186 +0,0 @@
-package tests
-
-import (
- "bytes"
- "fmt"
- "sort"
- "strconv"
- "strings"
-
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/lsp/source"
-)
-
-func ToProtocolCompletionItems(items []source.CompletionItem) []protocol.CompletionItem {
- var result []protocol.CompletionItem
- for _, item := range items {
- result = append(result, ToProtocolCompletionItem(item))
- }
- return result
-}
-
-func ToProtocolCompletionItem(item source.CompletionItem) protocol.CompletionItem {
- pItem := protocol.CompletionItem{
- Label: item.Label,
- Kind: item.Kind,
- Detail: item.Detail,
- Documentation: item.Documentation,
- InsertText: item.InsertText,
- TextEdit: &protocol.TextEdit{
- NewText: item.Snippet(),
- },
- // Negate score so best score has lowest sort text like real API.
- SortText: fmt.Sprint(-item.Score),
- }
- if pItem.InsertText == "" {
- pItem.InsertText = pItem.Label
- }
- return pItem
-}
-
-func FilterBuiltins(items []protocol.CompletionItem) []protocol.CompletionItem {
- var got []protocol.CompletionItem
- for _, item := range items {
- if isBuiltin(item.Label, item.Detail, item.Kind) {
- continue
- }
- got = append(got, item)
- }
- return got
-}
-
-func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool {
- if detail == "" && kind == protocol.ClassCompletion {
- return true
- }
- // Remaining builtin constants, variables, interfaces, and functions.
- trimmed := label
- if i := strings.Index(trimmed, "("); i >= 0 {
- trimmed = trimmed[:i]
- }
- switch trimmed {
- case "append", "cap", "close", "complex", "copy", "delete",
- "error", "false", "imag", "iota", "len", "make", "new",
- "nil", "panic", "print", "println", "real", "recover", "true":
- return true
- }
- return false
-}
-
-func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string {
- var (
- matchedIdxs []int
- lastGotIdx int
- lastGotSort float64
- inOrder = true
- errorMsg = "completions out of order"
- )
- for _, w := range want {
- var found bool
- for i, g := range got {
- if w.Label == g.Label && w.Detail == g.Detail && w.Kind == g.Kind {
- matchedIdxs = append(matchedIdxs, i)
- found = true
-
- if i < lastGotIdx {
- inOrder = false
- }
- lastGotIdx = i
-
- sort, _ := strconv.ParseFloat(g.SortText, 64)
- if strictScores && len(matchedIdxs) > 1 && sort <= lastGotSort {
- inOrder = false
- errorMsg = "candidate scores not strictly decreasing"
- }
- lastGotSort = sort
-
- break
- }
- }
- if !found {
- return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion")
- }
- }
-
- sort.Ints(matchedIdxs)
- matched := make([]protocol.CompletionItem, 0, len(matchedIdxs))
- for _, idx := range matchedIdxs {
- matched = append(matched, got[idx])
- }
-
- if !inOrder {
- return summarizeCompletionItems(-1, want, matched, errorMsg)
- }
-
- return ""
-}
-
-func DiffSnippets(want string, got *protocol.CompletionItem) string {
- if want == "" {
- if got != nil {
- return fmt.Sprintf("expected no snippet but got %s", got.TextEdit.NewText)
- }
- } else {
- if got == nil {
- return fmt.Sprintf("couldn't find completion matching %q", want)
- }
- if want != got.TextEdit.NewText {
- return fmt.Sprintf("expected snippet %q, got %q", want, got.TextEdit.NewText)
- }
- }
- return ""
-}
-
-func FindItem(list []protocol.CompletionItem, want source.CompletionItem) *protocol.CompletionItem {
- for _, item := range list {
- if item.Label == want.Label {
- return &item
- }
- }
- return nil
-}
-
-// DiffCompletionItems prints the diff between expected and actual completion
-// test results.
-func DiffCompletionItems(want, got []protocol.CompletionItem) string {
- if len(got) != len(want) {
- return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
- }
- for i, w := range want {
- g := got[i]
- if w.Label != g.Label {
- return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
- }
- if w.Detail != g.Detail {
- return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
- }
- if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
- if w.Documentation != g.Documentation {
- return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
- }
- }
- if w.Kind != g.Kind {
- return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind)
- }
- }
- return ""
-}
-
-func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string {
- msg := &bytes.Buffer{}
- fmt.Fprint(msg, "completion failed")
- if i >= 0 {
- fmt.Fprintf(msg, " at %d", i)
- }
- fmt.Fprint(msg, " because of ")
- fmt.Fprintf(msg, reason, args...)
- fmt.Fprint(msg, ":\nexpected:\n")
- for _, d := range want {
- fmt.Fprintf(msg, " %v\n", d)
- }
- fmt.Fprintf(msg, "got:\n")
- for _, d := range got {
- fmt.Fprintf(msg, " %v\n", d)
- }
- return msg.String()
-}
diff --git a/internal/lsp/tests/diagnostics.go b/internal/lsp/tests/diagnostics.go
deleted file mode 100644
index 192cde4..0000000
--- a/internal/lsp/tests/diagnostics.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package tests
-
-import (
- "bytes"
- "fmt"
- "strings"
-
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
-)
-
-// DiffDiagnostics prints the diff between expected and actual diagnostics test
-// results.
-func DiffDiagnostics(uri span.URI, want, got []source.Diagnostic) string {
- source.SortDiagnostics(want)
- source.SortDiagnostics(got)
-
- if len(got) != len(want) {
- return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
- }
- for i, w := range want {
- g := got[i]
- if w.Message != g.Message {
- return summarizeDiagnostics(i, uri, want, got, "incorrect Message got %v want %v", g.Message, w.Message)
- }
- if w.Severity != g.Severity {
- return summarizeDiagnostics(i, uri, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity)
- }
- if w.Source != g.Source {
- return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
- }
- // Don't check the range on the badimport test.
- if strings.Contains(uri.Filename(), "badimport") {
- continue
- }
- if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
- return summarizeDiagnostics(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
- }
- if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the diagnostic returns a zero-length range.
- if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
- return summarizeDiagnostics(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
- }
- }
- }
- return ""
-}
-
-func summarizeDiagnostics(i int, uri span.URI, want []source.Diagnostic, got []source.Diagnostic, reason string, args ...interface{}) string {
- msg := &bytes.Buffer{}
- fmt.Fprint(msg, "diagnostics failed")
- if i >= 0 {
- fmt.Fprintf(msg, " at %d", i)
- }
- fmt.Fprint(msg, " because of ")
- fmt.Fprintf(msg, reason, args...)
- fmt.Fprint(msg, ":\nexpected:\n")
- for _, d := range want {
- fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message)
- }
- fmt.Fprintf(msg, "got:\n")
- for _, d := range got {
- fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message)
- }
- return msg.String()
-}
diff --git a/internal/lsp/tests/links.go b/internal/lsp/tests/links.go
deleted file mode 100644
index 07fc3ef..0000000
--- a/internal/lsp/tests/links.go
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package tests
-
-import (
- "fmt"
- "go/token"
-
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/span"
-)
-
-// DiffLinks takes the links we got and checks if they are located within the source or a Note.
-// If the link is within a Note, the link is removed.
-// Returns an diff comment if there are differences and empty string if no diffs
-func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string {
- var notePositions []token.Position
- links := make(map[span.Span]string, len(wantLinks))
- for _, link := range wantLinks {
- links[link.Src] = link.Target
- notePositions = append(notePositions, link.NotePosition)
- }
- for _, link := range gotLinks {
- spn, err := mapper.RangeSpan(link.Range)
- if err != nil {
- return fmt.Sprintf("%v", err)
- }
- linkInNote := false
- for _, notePosition := range notePositions {
- // Drop the links found inside expectation notes arguments as this links are not collected by expect package
- if notePosition.Line == spn.Start().Line() &&
- notePosition.Column <= spn.Start().Column() {
- delete(links, spn)
- linkInNote = true
- }
- }
- if linkInNote {
- continue
- }
- if target, ok := links[spn]; ok {
- delete(links, spn)
- if target != link.Target {
- return fmt.Sprintf("for %v want %v, got %v\n", spn, link.Target, target)
- }
- } else {
- return fmt.Sprintf("unexpected link %v:%v\n", spn, link.Target)
- }
- }
- for spn, target := range links {
- return fmt.Sprintf("missing link %v:%v\n", spn, target)
- }
- return ""
-}
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index f4331da..6e863a7 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -15,6 +15,7 @@
"io/ioutil"
"os"
"path/filepath"
+ "regexp"
"runtime"
"sort"
"strconv"
@@ -36,11 +37,13 @@
overlayFileSuffix = ".overlay"
goldenFileSuffix = ".golden"
inFileSuffix = ".in"
+ summaryFile = "summary.txt"
testModule = "golang.org/x/tools/internal/lsp"
)
var UpdateGolden = flag.Bool("golden", false, "Update golden files")
+type CodeLens map[span.Span][]protocol.CodeLens
type Diagnostics map[span.URI][]source.Diagnostic
type CompletionItems map[token.Pos]*source.CompletionItem
type Completions map[span.Span][]Completion
@@ -62,46 +65,57 @@
type PrepareRenames map[span.Span]*source.PrepareItem
type Symbols map[span.URI][]protocol.DocumentSymbol
type SymbolsChildren map[string][]protocol.DocumentSymbol
-type Signatures map[span.Span]*source.SignatureInformation
+type SymbolInformation map[span.Span]protocol.SymbolInformation
+type WorkspaceSymbols map[string][]protocol.SymbolInformation
+type Signatures map[span.Span]*protocol.SignatureHelp
type Links map[span.URI][]Link
type Data struct {
- Config packages.Config
- Exported *packagestest.Exported
- Diagnostics Diagnostics
- CompletionItems CompletionItems
- Completions Completions
- CompletionSnippets CompletionSnippets
- UnimportedCompletions UnimportedCompletions
- DeepCompletions DeepCompletions
- FuzzyCompletions FuzzyCompletions
- CaseSensitiveCompletions CaseSensitiveCompletions
- RankCompletions RankCompletions
- FoldingRanges FoldingRanges
- Formats Formats
- Imports Imports
- SuggestedFixes SuggestedFixes
- Definitions Definitions
- Implementations Implementations
- Highlights Highlights
- References References
- Renames Renames
- PrepareRenames PrepareRenames
- Symbols Symbols
- symbolsChildren SymbolsChildren
- Signatures Signatures
- Links Links
+ Config packages.Config
+ Exported *packagestest.Exported
+ CodeLens CodeLens
+ Diagnostics Diagnostics
+ CompletionItems CompletionItems
+ Completions Completions
+ CompletionSnippets CompletionSnippets
+ UnimportedCompletions UnimportedCompletions
+ DeepCompletions DeepCompletions
+ FuzzyCompletions FuzzyCompletions
+ CaseSensitiveCompletions CaseSensitiveCompletions
+ RankCompletions RankCompletions
+ FoldingRanges FoldingRanges
+ Formats Formats
+ Imports Imports
+ SuggestedFixes SuggestedFixes
+ Definitions Definitions
+ Implementations Implementations
+ Highlights Highlights
+ References References
+ Renames Renames
+ PrepareRenames PrepareRenames
+ Symbols Symbols
+ symbolsChildren SymbolsChildren
+ symbolInformation SymbolInformation
+ WorkspaceSymbols WorkspaceSymbols
+ FuzzyWorkspaceSymbols WorkspaceSymbols
+ CaseSensitiveWorkspaceSymbols WorkspaceSymbols
+ Signatures Signatures
+ Links Links
t testing.TB
fragments map[string]string
dir string
+ Folder string
golden map[string]*Golden
+ ModfileFlagAvailable bool
+
mappersMu sync.Mutex
mappers map[span.URI]*protocol.ColumnMapper
}
type Tests interface {
+ CodeLens(*testing.T, span.Span, []protocol.CodeLens)
Diagnostics(*testing.T, span.URI, []source.Diagnostic)
Completion(*testing.T, span.Span, Completion, CompletionItems)
CompletionSnippet(*testing.T, span.Span, CompletionSnippet, bool, CompletionItems)
@@ -121,7 +135,10 @@
Rename(*testing.T, span.Span, string)
PrepareRename(*testing.T, span.Span, *source.PrepareItem)
Symbols(*testing.T, span.URI, []protocol.DocumentSymbol)
- SignatureHelp(*testing.T, span.Span, *source.SignatureInformation)
+ WorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})
+ FuzzyWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})
+ CaseSensitiveWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})
+ SignatureHelp(*testing.T, span.Span, *protocol.SignatureHelp)
Link(*testing.T, span.URI, []Link)
}
@@ -147,13 +164,26 @@
// Fuzzy tests deep completion and fuzzy matching.
CompletionFuzzy
- // CaseSensitive tests case sensitive completion
+ // CaseSensitive tests case sensitive completion.
CompletionCaseSensitive
// CompletionRank candidates in test must be valid and in the right relative order.
CompletionRank
)
+type WorkspaceSymbolsTestType int
+
+const (
+ // Default runs the standard workspace symbols tests.
+ WorkspaceSymbolsDefault = WorkspaceSymbolsTestType(iota)
+
+ // Fuzzy tests workspace symbols with fuzzy matching.
+ WorkspaceSymbolsFuzzy
+
+ // CaseSensitive tests workspace symbols with case sensitive.
+ WorkspaceSymbolsCaseSensitive
+)
+
type Completion struct {
CompletionItems []token.Pos
}
@@ -181,7 +211,7 @@
}
func DefaultOptions() source.Options {
- o := source.DefaultOptions
+ o := source.DefaultOptions()
o.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{
source.Go: {
protocol.SourceOrganizeImports: true,
@@ -200,145 +230,208 @@
var haveCgo = false
-func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
+// For Load() to properly create the folder structure required when testing with modules.
+// The directory structure of a test needs to look like the example below:
+//
+// - dir
+// - primarymod
+// - .go files
+// - packages
+// - go.mod (optional)
+// - modules
+// - repoa
+// - mod1
+// - .go files
+// - packages
+// - go.mod (optional)
+// - mod2
+// - repob
+// - mod1
+//
+// All the files that are primarily being tested should be in the primarymod folder,
+// any auxillary packages should be declared in the modules folder.
+// The modules folder requires each module to have the following format: repo/module
+// Then inside each repo/module, there can be any number of packages and files that are
+// needed to test the primarymod.
+func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data {
t.Helper()
- data := &Data{
- Diagnostics: make(Diagnostics),
- CompletionItems: make(CompletionItems),
- Completions: make(Completions),
- CompletionSnippets: make(CompletionSnippets),
- UnimportedCompletions: make(UnimportedCompletions),
- DeepCompletions: make(DeepCompletions),
- FuzzyCompletions: make(FuzzyCompletions),
- RankCompletions: make(RankCompletions),
- CaseSensitiveCompletions: make(CaseSensitiveCompletions),
- Definitions: make(Definitions),
- Implementations: make(Implementations),
- Highlights: make(Highlights),
- References: make(References),
- Renames: make(Renames),
- PrepareRenames: make(PrepareRenames),
- Symbols: make(Symbols),
- symbolsChildren: make(SymbolsChildren),
- Signatures: make(Signatures),
- Links: make(Links),
-
- t: t,
- dir: dir,
- fragments: map[string]string{},
- golden: map[string]*Golden{},
- mappers: map[span.URI]*protocol.ColumnMapper{},
+ folders, err := testFolders(dir)
+ if err != nil {
+ t.Fatalf("could not get test folders for %v, %v", dir, err)
}
- files := packagestest.MustCopyFileTree(dir)
- overlays := map[string][]byte{}
- for fragment, operation := range files {
- if trimmed := strings.TrimSuffix(fragment, goldenFileSuffix); trimmed != fragment {
- delete(files, fragment)
- goldFile := filepath.Join(dir, fragment)
- archive, err := txtar.ParseFile(goldFile)
- if err != nil {
- t.Fatalf("could not read golden file %v: %v", fragment, err)
+ var data []*Data
+ for _, folder := range folders {
+ datum := &Data{
+ CodeLens: make(CodeLens),
+ Diagnostics: make(Diagnostics),
+ CompletionItems: make(CompletionItems),
+ Completions: make(Completions),
+ CompletionSnippets: make(CompletionSnippets),
+ UnimportedCompletions: make(UnimportedCompletions),
+ DeepCompletions: make(DeepCompletions),
+ FuzzyCompletions: make(FuzzyCompletions),
+ RankCompletions: make(RankCompletions),
+ CaseSensitiveCompletions: make(CaseSensitiveCompletions),
+ Definitions: make(Definitions),
+ Implementations: make(Implementations),
+ Highlights: make(Highlights),
+ References: make(References),
+ Renames: make(Renames),
+ PrepareRenames: make(PrepareRenames),
+ Symbols: make(Symbols),
+ symbolsChildren: make(SymbolsChildren),
+ symbolInformation: make(SymbolInformation),
+ WorkspaceSymbols: make(WorkspaceSymbols),
+ FuzzyWorkspaceSymbols: make(WorkspaceSymbols),
+ CaseSensitiveWorkspaceSymbols: make(WorkspaceSymbols),
+ Signatures: make(Signatures),
+ Links: make(Links),
+
+ t: t,
+ dir: folder,
+ Folder: folder,
+ fragments: map[string]string{},
+ golden: map[string]*Golden{},
+ mappers: map[span.URI]*protocol.ColumnMapper{},
+ }
+
+ if !*UpdateGolden {
+ summary := filepath.Join(filepath.FromSlash(folder), summaryFile+goldenFileSuffix)
+ if _, err := os.Stat(summary); os.IsNotExist(err) {
+ t.Fatalf("could not find golden file summary.txt in %#v", folder)
}
- data.golden[trimmed] = &Golden{
- Filename: goldFile,
+ archive, err := txtar.ParseFile(summary)
+ if err != nil {
+ t.Fatalf("could not read golden file %v/%v: %v", folder, summary, err)
+ }
+ datum.golden[summaryFile] = &Golden{
+ Filename: summary,
Archive: archive,
}
- } else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment {
- delete(files, fragment)
- files[trimmed] = operation
- } else if index := strings.Index(fragment, overlayFileSuffix); index >= 0 {
- delete(files, fragment)
- partial := fragment[:index] + fragment[index+len(overlayFileSuffix):]
- contents, err := ioutil.ReadFile(filepath.Join(dir, fragment))
- if err != nil {
- t.Fatal(err)
+ }
+
+ modules, _ := packagestest.GroupFilesByModules(folder)
+ for i, m := range modules {
+ for fragment, operation := range m.Files {
+ if trimmed := strings.TrimSuffix(fragment, goldenFileSuffix); trimmed != fragment {
+ delete(m.Files, fragment)
+ goldFile := filepath.Join(m.Name, fragment)
+ if i == 0 {
+ goldFile = filepath.Join(m.Name, "primarymod", fragment)
+ }
+ archive, err := txtar.ParseFile(goldFile)
+ if err != nil {
+ t.Fatalf("could not read golden file %v: %v", fragment, err)
+ }
+ datum.golden[trimmed] = &Golden{
+ Filename: goldFile,
+ Archive: archive,
+ }
+ } else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment {
+ delete(m.Files, fragment)
+ m.Files[trimmed] = operation
+ } else if index := strings.Index(fragment, overlayFileSuffix); index >= 0 {
+ delete(m.Files, fragment)
+ partial := fragment[:index] + fragment[index+len(overlayFileSuffix):]
+ overlayFile := filepath.Join(m.Name, fragment)
+ if i == 0 {
+ overlayFile = filepath.Join(m.Name, "primarymod", fragment)
+ }
+ contents, err := ioutil.ReadFile(overlayFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ m.Overlay[partial] = contents
+ }
}
- overlays[partial] = contents
}
- }
- modules := []packagestest.Module{
- {
- Name: testModule,
- Files: files,
- Overlay: overlays,
- },
- {
- Name: "example.com/extramodule",
- Files: map[string]interface{}{
- "pkg/x.go": "package pkg\n",
+ if len(modules) > 0 {
+ // For certain LSP related tests to run, make sure that the primary
+ // module for the passed in directory is testModule.
+ modules[0].Name = testModule
+ }
+ // Add exampleModule to provide tests with another pkg.
+ datum.Exported = packagestest.Export(t, exporter, modules)
+ for _, m := range modules {
+ for fragment := range m.Files {
+ filename := datum.Exported.File(m.Name, fragment)
+ datum.fragments[filename] = fragment
+ }
+ }
+
+ // Turn off go/packages debug logging.
+ datum.Exported.Config.Logf = nil
+ datum.Config.Logf = nil
+
+ // Merge the exported.Config with the view.Config.
+ datum.Config = *datum.Exported.Config
+ datum.Config.Fset = token.NewFileSet()
+ datum.Config.Context = Context(nil)
+ datum.Config.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
+ panic("ParseFile should not be called")
+ }
+
+ // Do a first pass to collect special markers for completion and workspace symbols.
+ if err := datum.Exported.Expect(map[string]interface{}{
+ "item": func(name string, r packagestest.Range, _ []string) {
+ datum.Exported.Mark(name, r)
},
- },
- }
- data.Exported = packagestest.Export(t, exporter, modules)
- for fragment := range files {
- filename := data.Exported.File(testModule, fragment)
- data.fragments[filename] = fragment
- }
-
- // Turn off go/packages debug logging.
- data.Exported.Config.Logf = nil
- data.Config.Logf = nil
-
- // Merge the exported.Config with the view.Config.
- data.Config = *data.Exported.Config
- data.Config.Fset = token.NewFileSet()
- data.Config.Context = Context(nil)
- data.Config.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
- panic("ParseFile should not be called")
- }
-
- // Do a first pass to collect special markers for completion.
- if err := data.Exported.Expect(map[string]interface{}{
- "item": func(name string, r packagestest.Range, _ []string) {
- data.Exported.Mark(name, r)
- },
- }); err != nil {
- t.Fatal(err)
- }
-
- // Collect any data that needs to be used by subsequent tests.
- if err := data.Exported.Expect(map[string]interface{}{
- "diag": data.collectDiagnostics,
- "item": data.collectCompletionItems,
- "complete": data.collectCompletions(CompletionDefault),
- "unimported": data.collectCompletions(CompletionUnimported),
- "deep": data.collectCompletions(CompletionDeep),
- "fuzzy": data.collectCompletions(CompletionFuzzy),
- "casesensitive": data.collectCompletions(CompletionCaseSensitive),
- "rank": data.collectCompletions(CompletionRank),
- "snippet": data.collectCompletionSnippets,
- "fold": data.collectFoldingRanges,
- "format": data.collectFormats,
- "import": data.collectImports,
- "godef": data.collectDefinitions,
- "implementations": data.collectImplementations,
- "typdef": data.collectTypeDefinitions,
- "hover": data.collectHoverDefinitions,
- "highlight": data.collectHighlights,
- "refs": data.collectReferences,
- "rename": data.collectRenames,
- "prepare": data.collectPrepareRenames,
- "symbol": data.collectSymbols,
- "signature": data.collectSignatures,
- "link": data.collectLinks,
- "suggestedfix": data.collectSuggestedFixes,
- }); err != nil {
- t.Fatal(err)
- }
- for _, symbols := range data.Symbols {
- for i := range symbols {
- children := data.symbolsChildren[symbols[i].Name]
- symbols[i].Children = children
+ "symbol": func(name string, r packagestest.Range, _ []string) {
+ datum.Exported.Mark(name, r)
+ },
+ }); err != nil {
+ t.Fatal(err)
}
- }
- // Collect names for the entries that require golden files.
- if err := data.Exported.Expect(map[string]interface{}{
- "godef": data.collectDefinitionNames,
- "hover": data.collectDefinitionNames,
- }); err != nil {
- t.Fatal(err)
+
+ // Collect any data that needs to be used by subsequent tests.
+ if err := datum.Exported.Expect(map[string]interface{}{
+ "codelens": datum.collectCodeLens,
+ "diag": datum.collectDiagnostics,
+ "item": datum.collectCompletionItems,
+ "complete": datum.collectCompletions(CompletionDefault),
+ "unimported": datum.collectCompletions(CompletionUnimported),
+ "deep": datum.collectCompletions(CompletionDeep),
+ "fuzzy": datum.collectCompletions(CompletionFuzzy),
+ "casesensitive": datum.collectCompletions(CompletionCaseSensitive),
+ "rank": datum.collectCompletions(CompletionRank),
+ "snippet": datum.collectCompletionSnippets,
+ "fold": datum.collectFoldingRanges,
+ "format": datum.collectFormats,
+ "import": datum.collectImports,
+ "godef": datum.collectDefinitions,
+ "implementations": datum.collectImplementations,
+ "typdef": datum.collectTypeDefinitions,
+ "hover": datum.collectHoverDefinitions,
+ "highlight": datum.collectHighlights,
+ "refs": datum.collectReferences,
+ "rename": datum.collectRenames,
+ "prepare": datum.collectPrepareRenames,
+ "symbol": datum.collectSymbols,
+ "signature": datum.collectSignatures,
+ "link": datum.collectLinks,
+ "suggestedfix": datum.collectSuggestedFixes,
+ }); err != nil {
+ t.Fatal(err)
+ }
+ for _, symbols := range datum.Symbols {
+ for i := range symbols {
+ children := datum.symbolsChildren[symbols[i].Name]
+ symbols[i].Children = children
+ }
+ }
+ // Collect names for the entries that require golden files.
+ if err := datum.Exported.Expect(map[string]interface{}{
+ "godef": datum.collectDefinitionNames,
+ "hover": datum.collectDefinitionNames,
+ "workspacesymbol": datum.collectWorkspaceSymbols(WorkspaceSymbolsDefault),
+ "workspacesymbolfuzzy": datum.collectWorkspaceSymbols(WorkspaceSymbolsFuzzy),
+ "workspacesymbolcasesensitive": datum.collectWorkspaceSymbols(WorkspaceSymbolsCaseSensitive),
+ }); err != nil {
+ t.Fatal(err)
+ }
+ data = append(data, datum)
}
return data
}
@@ -352,7 +445,7 @@
for src, exp := range cases {
for i, e := range exp {
- t.Run(spanName(src)+"_"+strconv.Itoa(i), func(t *testing.T) {
+ t.Run(SpanName(src)+"_"+strconv.Itoa(i), func(t *testing.T) {
t.Helper()
if (!haveCgo || runtime.GOOS == "android") && strings.Contains(t.Name(), "cgo") {
t.Skip("test requires cgo, not supported")
@@ -364,6 +457,28 @@
}
}
+ eachWorkspaceSymbols := func(t *testing.T, cases map[string][]protocol.SymbolInformation, test func(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})) {
+ t.Helper()
+
+ for query, expectedSymbols := range cases {
+ name := query
+ if name == "" {
+ name = "EmptyQuery"
+ }
+ t.Run(name, func(t *testing.T) {
+ t.Helper()
+ dirs := make(map[string]struct{})
+ for _, si := range expectedSymbols {
+ d := filepath.Dir(si.Location.URI.SpanURI().Filename())
+ if _, ok := dirs[d]; !ok {
+ dirs[d] = struct{}{}
+ }
+ }
+ test(t, query, expectedSymbols, dirs)
+ })
+ }
+ }
+
t.Run("Completion", func(t *testing.T) {
t.Helper()
eachCompletion(t, data.Completions, tests.Completion)
@@ -374,7 +489,7 @@
for _, placeholders := range []bool{true, false} {
for src, expecteds := range data.CompletionSnippets {
for i, expected := range expecteds {
- name := spanName(src) + "_" + strconv.Itoa(i+1)
+ name := SpanName(src) + "_" + strconv.Itoa(i+1)
if placeholders {
name += "_placeholders"
}
@@ -413,9 +528,27 @@
eachCompletion(t, data.RankCompletions, tests.RankCompletion)
})
+ t.Run("CodeLens", func(t *testing.T) {
+ t.Helper()
+ for spn, want := range data.CodeLens {
+ // Check if we should skip this URI if the -modfile flag is not available.
+ if shouldSkip(data, spn.URI()) {
+ continue
+ }
+ t.Run(SpanName(spn), func(t *testing.T) {
+ t.Helper()
+ tests.CodeLens(t, spn, want)
+ })
+ }
+ })
+
t.Run("Diagnostics", func(t *testing.T) {
t.Helper()
for uri, want := range data.Diagnostics {
+ // Check if we should skip this URI if the -modfile flag is not available.
+ if shouldSkip(data, uri) {
+ continue
+ }
t.Run(uriName(uri), func(t *testing.T) {
t.Helper()
tests.Diagnostics(t, uri, want)
@@ -456,7 +589,11 @@
t.Run("SuggestedFix", func(t *testing.T) {
t.Helper()
for _, spn := range data.SuggestedFixes {
- t.Run(spanName(spn), func(t *testing.T) {
+ // Check if we should skip this spn if the -modfile flag is not available.
+ if shouldSkip(data, spn.URI()) {
+ continue
+ }
+ t.Run(SpanName(spn), func(t *testing.T) {
t.Helper()
tests.SuggestedFix(t, spn)
})
@@ -466,7 +603,7 @@
t.Run("Definition", func(t *testing.T) {
t.Helper()
for spn, d := range data.Definitions {
- t.Run(spanName(spn), func(t *testing.T) {
+ t.Run(SpanName(spn), func(t *testing.T) {
t.Helper()
if (!haveCgo || runtime.GOOS == "android") && strings.Contains(t.Name(), "cgo") {
t.Skip("test requires cgo, not supported")
@@ -479,7 +616,7 @@
t.Run("Implementation", func(t *testing.T) {
t.Helper()
for spn, m := range data.Implementations {
- t.Run(spanName(spn), func(t *testing.T) {
+ t.Run(SpanName(spn), func(t *testing.T) {
t.Helper()
tests.Implementation(t, spn, m)
})
@@ -489,7 +626,7 @@
t.Run("Highlight", func(t *testing.T) {
t.Helper()
for pos, locations := range data.Highlights {
- t.Run(spanName(pos), func(t *testing.T) {
+ t.Run(SpanName(pos), func(t *testing.T) {
t.Helper()
tests.Highlight(t, pos, locations)
})
@@ -499,7 +636,7 @@
t.Run("References", func(t *testing.T) {
t.Helper()
for src, itemList := range data.References {
- t.Run(spanName(src), func(t *testing.T) {
+ t.Run(SpanName(src), func(t *testing.T) {
t.Helper()
tests.References(t, src, itemList)
})
@@ -519,7 +656,7 @@
t.Run("PrepareRenames", func(t *testing.T) {
t.Helper()
for src, want := range data.PrepareRenames {
- t.Run(spanName(src), func(t *testing.T) {
+ t.Run(SpanName(src), func(t *testing.T) {
t.Helper()
tests.PrepareRename(t, src, want)
})
@@ -536,10 +673,25 @@
}
})
+ t.Run("WorkspaceSymbols", func(t *testing.T) {
+ t.Helper()
+ eachWorkspaceSymbols(t, data.WorkspaceSymbols, tests.WorkspaceSymbols)
+ })
+
+ t.Run("FuzzyWorkspaceSymbols", func(t *testing.T) {
+ t.Helper()
+ eachWorkspaceSymbols(t, data.FuzzyWorkspaceSymbols, tests.FuzzyWorkspaceSymbols)
+ })
+
+ t.Run("CaseSensitiveWorkspaceSymbols", func(t *testing.T) {
+ t.Helper()
+ eachWorkspaceSymbols(t, data.CaseSensitiveWorkspaceSymbols, tests.CaseSensitiveWorkspaceSymbols)
+ })
+
t.Run("SignatureHelp", func(t *testing.T) {
t.Helper()
for spn, expectedSignature := range data.Signatures {
- t.Run(spanName(spn), func(t *testing.T) {
+ t.Run(SpanName(spn), func(t *testing.T) {
t.Helper()
tests.SignatureHelp(t, spn, expectedSignature)
})
@@ -549,6 +701,18 @@
t.Run("Link", func(t *testing.T) {
t.Helper()
for uri, wantLinks := range data.Links {
+ // If we are testing GOPATH, then we do not want links with
+ // the versions attached (pkg.go.dev/repoa/moda@v1.1.0/pkg),
+ // unless the file is a go.mod, then we can skip it alltogether.
+ if data.Exported.Exporter == packagestest.GOPATH {
+ if strings.HasSuffix(uri.Filename(), ".mod") {
+ continue
+ }
+ re := regexp.MustCompile(`@v\d+\.\d+\.[\w-]+`)
+ for i, link := range wantLinks {
+ wantLinks[i].Target = re.ReplaceAllString(link.Target, "")
+ }
+ }
t.Run(uriName(uri), func(t *testing.T) {
t.Helper()
tests.Link(t, uri, wantLinks)
@@ -603,6 +767,14 @@
return count
}
+ countCodeLens := func(c map[span.Span][]protocol.CodeLens) (count int) {
+ for _, want := range c {
+ count += len(want)
+ }
+ return count
+ }
+
+ fmt.Fprintf(buf, "CodeLensCount = %v\n", countCodeLens(data.CodeLens))
fmt.Fprintf(buf, "CompletionsCount = %v\n", countCompletions(data.Completions))
fmt.Fprintf(buf, "CompletionSnippetCount = %v\n", snippetCount)
fmt.Fprintf(buf, "UnimportedCompletionsCount = %v\n", countCompletions(data.UnimportedCompletions))
@@ -622,11 +794,14 @@
fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames))
fmt.Fprintf(buf, "PrepareRenamesCount = %v\n", len(data.PrepareRenames))
fmt.Fprintf(buf, "SymbolsCount = %v\n", len(data.Symbols))
+ fmt.Fprintf(buf, "WorkspaceSymbolsCount = %v\n", len(data.WorkspaceSymbols))
+ fmt.Fprintf(buf, "FuzzyWorkspaceSymbolsCount = %v\n", len(data.FuzzyWorkspaceSymbols))
+ fmt.Fprintf(buf, "CaseSensitiveWorkspaceSymbolsCount = %v\n", len(data.CaseSensitiveWorkspaceSymbols))
fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures))
fmt.Fprintf(buf, "LinksCount = %v\n", linksCount)
fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations))
- want := string(data.Golden("summary", "summary.txt", func() ([]byte, error) {
+ want := string(data.Golden("summary", summaryFile, func() ([]byte, error) {
return buf.Bytes(), nil
}))
got := buf.String()
@@ -668,8 +843,12 @@
if !*UpdateGolden {
data.t.Fatalf("could not find golden file %v: %v", fragment, tag)
}
+ var subdir string
+ if fragment != summaryFile {
+ subdir = "primarymod"
+ }
golden = &Golden{
- Filename: filepath.Join(data.dir, fragment+goldenFileSuffix),
+ Filename: filepath.Join(data.dir, subdir, fragment+goldenFileSuffix),
Archive: &txtar.Archive{},
Modified: true,
}
@@ -703,27 +882,53 @@
return file.Data[:len(file.Data)-1] // drop the trailing \n
}
-func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg string) {
+func (data *Data) collectCodeLens(spn span.Span, title, cmd string) {
+ if _, ok := data.CodeLens[spn]; !ok {
+ data.CodeLens[spn] = []protocol.CodeLens{}
+ }
+ m, err := data.Mapper(spn.URI())
+ if err != nil {
+ return
+ }
+ rng, err := m.Range(spn)
+ if err != nil {
+ return
+ }
+ data.CodeLens[spn] = append(data.CodeLens[spn], protocol.CodeLens{
+ Range: rng,
+ Command: protocol.Command{
+ Title: title,
+ Command: cmd,
+ },
+ })
+}
+
+func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg, msgSeverity string) {
if _, ok := data.Diagnostics[spn.URI()]; !ok {
data.Diagnostics[spn.URI()] = []source.Diagnostic{}
}
- severity := protocol.SeverityError
- if strings.Contains(string(spn.URI()), "analyzer") {
- severity = protocol.SeverityWarning
+ m, err := data.Mapper(spn.URI())
+ if err != nil {
+ return
}
- // This is not the correct way to do this,
- // but it seems excessive to do the full conversion here.
+ rng, err := m.Range(spn)
+ if err != nil {
+ return
+ }
+ severity := protocol.SeverityError
+ switch msgSeverity {
+ case "error":
+ severity = protocol.SeverityError
+ case "warning":
+ severity = protocol.SeverityWarning
+ case "hint":
+ severity = protocol.SeverityHint
+ case "information":
+ severity = protocol.SeverityInformation
+ }
+ // This is not the correct way to do this, but it seems excessive to do the full conversion here.
want := source.Diagnostic{
- Range: protocol.Range{
- Start: protocol.Position{
- Line: float64(spn.Start().Line()) - 1,
- Character: float64(spn.Start().Column()) - 1,
- },
- End: protocol.Position{
- Line: float64(spn.End().Line()) - 1,
- Character: float64(spn.End().Column()) - 1,
- },
- },
+ Range: rng,
Severity: severity,
Source: msgSource,
Message: msg,
@@ -885,12 +1090,50 @@
} else {
data.symbolsChildren[parentName] = append(data.symbolsChildren[parentName], sym)
}
+
+ // Reuse @symbol in the workspace symbols tests.
+ si := protocol.SymbolInformation{
+ Name: sym.Name,
+ Kind: sym.Kind,
+ Location: protocol.Location{
+ URI: protocol.URIFromSpanURI(spn.URI()),
+ Range: sym.SelectionRange,
+ },
+ }
+ data.symbolInformation[spn] = si
+}
+
+func (data *Data) collectWorkspaceSymbols(typ WorkspaceSymbolsTestType) func(string, []span.Span) {
+ switch typ {
+ case WorkspaceSymbolsFuzzy:
+ return func(query string, targets []span.Span) {
+ for _, target := range targets {
+ data.FuzzyWorkspaceSymbols[query] = append(data.FuzzyWorkspaceSymbols[query], data.symbolInformation[target])
+ }
+ }
+ case WorkspaceSymbolsCaseSensitive:
+ return func(query string, targets []span.Span) {
+ for _, target := range targets {
+ data.CaseSensitiveWorkspaceSymbols[query] = append(data.CaseSensitiveWorkspaceSymbols[query], data.symbolInformation[target])
+ }
+ }
+ default:
+ return func(query string, targets []span.Span) {
+ for _, target := range targets {
+ data.WorkspaceSymbols[query] = append(data.WorkspaceSymbols[query], data.symbolInformation[target])
+ }
+ }
+ }
}
func (data *Data) collectSignatures(spn span.Span, signature string, activeParam int64) {
- data.Signatures[spn] = &source.SignatureInformation{
- Label: signature,
- ActiveParameter: int(activeParam),
+ data.Signatures[spn] = &protocol.SignatureHelp{
+ Signatures: []protocol.SignatureInformation{
+ {
+ Label: signature,
+ },
+ },
+ ActiveParameter: float64(activeParam),
}
// Hardcode special case to test the lack of a signature.
if signature == "" && activeParam == 0 {
@@ -920,7 +1163,7 @@
return filepath.Base(strings.TrimSuffix(uri.Filename(), ".go"))
}
-func spanName(spn span.Span) string {
+func SpanName(spn span.Span) string {
return fmt.Sprintf("%v_%v_%v", uriName(spn.URI()), spn.Start().Line(), spn.Start().Column())
}
@@ -955,3 +1198,40 @@
}
return dst, nil
}
+
+func testFolders(root string) ([]string, error) {
+ // Check if this only has one test directory.
+ if _, err := os.Stat(filepath.Join(filepath.FromSlash(root), "primarymod")); !os.IsNotExist(err) {
+ return []string{root}, nil
+ }
+ folders := []string{}
+ root = filepath.FromSlash(root)
+ // Get all test directories that are one level deeper than root.
+ if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if !info.IsDir() {
+ return nil
+ }
+ if filepath.Dir(path) == root {
+ folders = append(folders, filepath.ToSlash(path))
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ return folders, nil
+}
+
+func shouldSkip(data *Data, uri span.URI) bool {
+ if data.ModfileFlagAvailable {
+ return false
+ }
+ // If the -modfile flag is not available, then we do not want to run
+ // any tests on the go.mod file.
+ if strings.HasSuffix(uri.Filename(), ".mod") {
+ return true
+ }
+ // If the -modfile flag is not available, then we do not want to test any
+ // uri that contains "go mod tidy".
+ m, err := data.Mapper(uri)
+ return err == nil && strings.Contains(string(m.Content), ", \"go mod tidy\",")
+}
diff --git a/internal/lsp/tests/util.go b/internal/lsp/tests/util.go
new file mode 100644
index 0000000..b81cfd0
--- /dev/null
+++ b/internal/lsp/tests/util.go
@@ -0,0 +1,496 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tests
+
+import (
+ "bytes"
+ "fmt"
+ "go/token"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/span"
+)
+
+// DiffLinks takes the links we got and checks if they are located within the source or a Note.
+// If the link is within a Note, the link is removed.
+// Returns an diff comment if there are differences and empty string if no diffs.
+func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string {
+ var notePositions []token.Position
+ links := make(map[span.Span]string, len(wantLinks))
+ for _, link := range wantLinks {
+ links[link.Src] = link.Target
+ notePositions = append(notePositions, link.NotePosition)
+ }
+ for _, link := range gotLinks {
+ spn, err := mapper.RangeSpan(link.Range)
+ if err != nil {
+ return fmt.Sprintf("%v", err)
+ }
+ linkInNote := false
+ for _, notePosition := range notePositions {
+ // Drop the links found inside expectation notes arguments as this links are not collected by expect package.
+ if notePosition.Line == spn.Start().Line() &&
+ notePosition.Column <= spn.Start().Column() {
+ delete(links, spn)
+ linkInNote = true
+ }
+ }
+ if linkInNote {
+ continue
+ }
+ if target, ok := links[spn]; ok {
+ delete(links, spn)
+ if target != link.Target {
+ return fmt.Sprintf("for %v want %v, got %v\n", spn, target, link.Target)
+ }
+ } else {
+ return fmt.Sprintf("unexpected link %v:%v\n", spn, link.Target)
+ }
+ }
+ for spn, target := range links {
+ return fmt.Sprintf("missing link %v:%v\n", spn, target)
+ }
+ return ""
+}
+
+// DiffSymbols prints the diff between expected and actual symbols test results.
+func DiffSymbols(t *testing.T, uri span.URI, want, got []protocol.DocumentSymbol) string {
+ sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name })
+ sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name })
+ if len(got) != len(want) {
+ return summarizeSymbols(t, -1, want, got, "different lengths got %v want %v", len(got), len(want))
+ }
+ for i, w := range want {
+ g := got[i]
+ if w.Name != g.Name {
+ return summarizeSymbols(t, i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
+ }
+ if w.Kind != g.Kind {
+ return summarizeSymbols(t, i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
+ }
+ if protocol.CompareRange(w.SelectionRange, g.SelectionRange) != 0 {
+ return summarizeSymbols(t, i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange)
+ }
+ if msg := DiffSymbols(t, uri, w.Children, g.Children); msg != "" {
+ return fmt.Sprintf("children of %s: %s", w.Name, msg)
+ }
+ }
+ return ""
+}
+
+func summarizeSymbols(t *testing.T, i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
+ msg := &bytes.Buffer{}
+ fmt.Fprint(msg, "document symbols failed")
+ if i >= 0 {
+ fmt.Fprintf(msg, " at %d", i)
+ }
+ fmt.Fprint(msg, " because of ")
+ fmt.Fprintf(msg, reason, args...)
+ fmt.Fprint(msg, ":\nexpected:\n")
+ for _, s := range want {
+ fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
+ }
+ fmt.Fprintf(msg, "got:\n")
+ for _, s := range got {
+ fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
+ }
+ return msg.String()
+}
+
+// FilterWorkspaceSymbols filters to got contained in the given dirs.
+func FilterWorkspaceSymbols(got []protocol.SymbolInformation, dirs map[string]struct{}) []protocol.SymbolInformation {
+ var result []protocol.SymbolInformation
+ for _, si := range got {
+ if _, ok := dirs[filepath.Dir(si.Location.URI.SpanURI().Filename())]; ok {
+ result = append(result, si)
+ }
+ }
+ return result
+}
+
+// DiffWorkspaceSymbols prints the diff between expected and actual workspace
+// symbols test results.
+func DiffWorkspaceSymbols(want, got []protocol.SymbolInformation) string {
+ sort.Slice(want, func(i, j int) bool { return fmt.Sprintf("%v", want[i]) < fmt.Sprintf("%v", want[j]) })
+ sort.Slice(got, func(i, j int) bool { return fmt.Sprintf("%v", got[i]) < fmt.Sprintf("%v", got[j]) })
+ if len(got) != len(want) {
+ return summarizeWorkspaceSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want))
+ }
+ for i, w := range want {
+ g := got[i]
+ if w.Name != g.Name {
+ return summarizeWorkspaceSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
+ }
+ if w.Kind != g.Kind {
+ return summarizeWorkspaceSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
+ }
+ if w.Location.URI != g.Location.URI {
+ return summarizeWorkspaceSymbols(i, want, got, "incorrect uri got %v want %v", g.Location.URI, w.Location.URI)
+ }
+ if protocol.CompareRange(w.Location.Range, g.Location.Range) != 0 {
+ return summarizeWorkspaceSymbols(i, want, got, "incorrect range got %v want %v", g.Location.Range, w.Location.Range)
+ }
+ }
+ return ""
+}
+
+func summarizeWorkspaceSymbols(i int, want, got []protocol.SymbolInformation, reason string, args ...interface{}) string {
+ msg := &bytes.Buffer{}
+ fmt.Fprint(msg, "workspace symbols failed")
+ if i >= 0 {
+ fmt.Fprintf(msg, " at %d", i)
+ }
+ fmt.Fprint(msg, " because of ")
+ fmt.Fprintf(msg, reason, args...)
+ fmt.Fprint(msg, ":\nexpected:\n")
+ for _, s := range want {
+ fmt.Fprintf(msg, " %v %v %v:%v\n", s.Name, s.Kind, s.Location.URI, s.Location.Range)
+ }
+ fmt.Fprintf(msg, "got:\n")
+ for _, s := range got {
+ fmt.Fprintf(msg, " %v %v %v:%v\n", s.Name, s.Kind, s.Location.URI, s.Location.Range)
+ }
+ return msg.String()
+}
+
+// DiffDiagnostics prints the diff between expected and actual diagnostics test
+// results.
+func DiffDiagnostics(uri span.URI, want, got []source.Diagnostic) string {
+ source.SortDiagnostics(want)
+ source.SortDiagnostics(got)
+
+ if len(got) != len(want) {
+ return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
+ }
+ for i, w := range want {
+ g := got[i]
+ if w.Message != g.Message {
+ return summarizeDiagnostics(i, uri, want, got, "incorrect Message got %v want %v", g.Message, w.Message)
+ }
+ if w.Severity != g.Severity {
+ return summarizeDiagnostics(i, uri, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity)
+ }
+ if w.Source != g.Source {
+ return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
+ }
+ // Don't check the range on the badimport test.
+ if strings.Contains(uri.Filename(), "badimport") {
+ continue
+ }
+ if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
+ return summarizeDiagnostics(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
+ }
+ if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the diagnostic returns a zero-length range.
+ if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
+ return summarizeDiagnostics(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
+ }
+ }
+ }
+ return ""
+}
+
+func summarizeDiagnostics(i int, uri span.URI, want []source.Diagnostic, got []source.Diagnostic, reason string, args ...interface{}) string {
+ msg := &bytes.Buffer{}
+ fmt.Fprint(msg, "diagnostics failed")
+ if i >= 0 {
+ fmt.Fprintf(msg, " at %d", i)
+ }
+ fmt.Fprint(msg, " because of ")
+ fmt.Fprintf(msg, reason, args...)
+ fmt.Fprint(msg, ":\nexpected:\n")
+ for _, d := range want {
+ fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message)
+ }
+ fmt.Fprintf(msg, "got:\n")
+ for _, d := range got {
+ fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message)
+ }
+ return msg.String()
+}
+
+func DiffCodeLens(uri span.URI, want, got []protocol.CodeLens) string {
+ sortCodeLens(want)
+ sortCodeLens(got)
+
+ if len(got) != len(want) {
+ return summarizeCodeLens(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
+ }
+ for i, w := range want {
+ g := got[i]
+ if w.Command.Title != g.Command.Title {
+ return summarizeCodeLens(i, uri, want, got, "incorrect Command Title got %v want %v", g.Command.Title, w.Command.Title)
+ }
+ if w.Command.Command != g.Command.Command {
+ return summarizeCodeLens(i, uri, want, got, "incorrect Command Title got %v want %v", g.Command.Command, w.Command.Command)
+ }
+ if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
+ return summarizeCodeLens(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
+ }
+ if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the codelens returns a zero-length range.
+ if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
+ return summarizeCodeLens(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
+ }
+ }
+ }
+ return ""
+}
+
+func sortCodeLens(c []protocol.CodeLens) {
+ sort.Slice(c, func(i int, j int) bool {
+ if r := protocol.CompareRange(c[i].Range, c[j].Range); r != 0 {
+ return r < 0
+ }
+ if c[i].Command.Command < c[j].Command.Command {
+ return true
+ }
+ if c[i].Command.Command == c[j].Command.Command {
+ return true
+ }
+ return c[i].Command.Title <= c[j].Command.Title
+ })
+}
+
+func summarizeCodeLens(i int, uri span.URI, want, got []protocol.CodeLens, reason string, args ...interface{}) string {
+ msg := &bytes.Buffer{}
+ fmt.Fprint(msg, "codelens failed")
+ if i >= 0 {
+ fmt.Fprintf(msg, " at %d", i)
+ }
+ fmt.Fprint(msg, " because of ")
+ fmt.Fprintf(msg, reason, args...)
+ fmt.Fprint(msg, ":\nexpected:\n")
+ for _, d := range want {
+ fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
+ }
+ fmt.Fprintf(msg, "got:\n")
+ for _, d := range got {
+ fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
+ }
+ return msg.String()
+}
+
+func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) string {
+ decorate := func(f string, args ...interface{}) string {
+ return fmt.Sprintf("Invalid signature at %s: %s", spn, fmt.Sprintf(f, args...))
+ }
+
+ if len(got.Signatures) != 1 {
+ return decorate("wanted 1 signature, got %d", len(got.Signatures))
+ }
+
+ if got.ActiveSignature != 0 {
+ return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature))
+ }
+
+ if want.ActiveParameter != got.ActiveParameter {
+ return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter))
+ }
+
+ gotSig := got.Signatures[int(got.ActiveSignature)]
+
+ if want.Signatures[0].Label != got.Signatures[0].Label {
+ return decorate("wanted label %q, got %q", want.Signatures[0].Label, got.Signatures[0].Label)
+ }
+
+ var paramParts []string
+ for _, p := range gotSig.Parameters {
+ paramParts = append(paramParts, p.Label)
+ }
+ paramsStr := strings.Join(paramParts, ", ")
+ if !strings.Contains(gotSig.Label, paramsStr) {
+ return decorate("expected signature %q to contain params %q", gotSig.Label, paramsStr)
+ }
+
+ return ""
+}
+
+func ToProtocolCompletionItems(items []source.CompletionItem) []protocol.CompletionItem {
+ var result []protocol.CompletionItem
+ for _, item := range items {
+ result = append(result, ToProtocolCompletionItem(item))
+ }
+ return result
+}
+
+func ToProtocolCompletionItem(item source.CompletionItem) protocol.CompletionItem {
+ pItem := protocol.CompletionItem{
+ Label: item.Label,
+ Kind: item.Kind,
+ Detail: item.Detail,
+ Documentation: item.Documentation,
+ InsertText: item.InsertText,
+ TextEdit: &protocol.TextEdit{
+ NewText: item.Snippet(),
+ },
+ // Negate score so best score has lowest sort text like real API.
+ SortText: fmt.Sprint(-item.Score),
+ }
+ if pItem.InsertText == "" {
+ pItem.InsertText = pItem.Label
+ }
+ return pItem
+}
+
+func FilterBuiltins(src span.Span, items []protocol.CompletionItem) []protocol.CompletionItem {
+ var (
+ got []protocol.CompletionItem
+ wantBuiltins = strings.Contains(string(src.URI()), "builtins")
+ wantKeywords = strings.Contains(string(src.URI()), "keywords")
+ )
+ for _, item := range items {
+ if !wantBuiltins && isBuiltin(item.Label, item.Detail, item.Kind) {
+ continue
+ }
+
+ if !wantKeywords && token.Lookup(item.Label).IsKeyword() {
+ continue
+ }
+
+ got = append(got, item)
+ }
+ return got
+}
+
+func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool {
+ if detail == "" && kind == protocol.ClassCompletion {
+ return true
+ }
+ // Remaining builtin constants, variables, interfaces, and functions.
+ trimmed := label
+ if i := strings.Index(trimmed, "("); i >= 0 {
+ trimmed = trimmed[:i]
+ }
+ switch trimmed {
+ case "append", "cap", "close", "complex", "copy", "delete",
+ "error", "false", "imag", "iota", "len", "make", "new",
+ "nil", "panic", "print", "println", "real", "recover", "true":
+ return true
+ }
+ return false
+}
+
+func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string {
+ var (
+ matchedIdxs []int
+ lastGotIdx int
+ lastGotSort float64
+ inOrder = true
+ errorMsg = "completions out of order"
+ )
+ for _, w := range want {
+ var found bool
+ for i, g := range got {
+ if w.Label == g.Label && w.Detail == g.Detail && w.Kind == g.Kind {
+ matchedIdxs = append(matchedIdxs, i)
+ found = true
+
+ if i < lastGotIdx {
+ inOrder = false
+ }
+ lastGotIdx = i
+
+ sort, _ := strconv.ParseFloat(g.SortText, 64)
+ if strictScores && len(matchedIdxs) > 1 && sort <= lastGotSort {
+ inOrder = false
+ errorMsg = "candidate scores not strictly decreasing"
+ }
+ lastGotSort = sort
+
+ break
+ }
+ }
+ if !found {
+ return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion")
+ }
+ }
+
+ sort.Ints(matchedIdxs)
+ matched := make([]protocol.CompletionItem, 0, len(matchedIdxs))
+ for _, idx := range matchedIdxs {
+ matched = append(matched, got[idx])
+ }
+
+ if !inOrder {
+ return summarizeCompletionItems(-1, want, matched, errorMsg)
+ }
+
+ return ""
+}
+
+func DiffSnippets(want string, got *protocol.CompletionItem) string {
+ if want == "" {
+ if got != nil {
+ return fmt.Sprintf("expected no snippet but got %s", got.TextEdit.NewText)
+ }
+ } else {
+ if got == nil {
+ return fmt.Sprintf("couldn't find completion matching %q", want)
+ }
+ if want != got.TextEdit.NewText {
+ return fmt.Sprintf("expected snippet %q, got %q", want, got.TextEdit.NewText)
+ }
+ }
+ return ""
+}
+
+func FindItem(list []protocol.CompletionItem, want source.CompletionItem) *protocol.CompletionItem {
+ for _, item := range list {
+ if item.Label == want.Label {
+ return &item
+ }
+ }
+ return nil
+}
+
+// DiffCompletionItems prints the diff between expected and actual completion
+// test results.
+func DiffCompletionItems(want, got []protocol.CompletionItem) string {
+ if len(got) != len(want) {
+ return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
+ }
+ for i, w := range want {
+ g := got[i]
+ if w.Label != g.Label {
+ return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
+ }
+ if w.Detail != g.Detail {
+ return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
+ }
+ if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
+ if w.Documentation != g.Documentation {
+ return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
+ }
+ }
+ if w.Kind != g.Kind {
+ return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind)
+ }
+ }
+ return ""
+}
+
+func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string {
+ msg := &bytes.Buffer{}
+ fmt.Fprint(msg, "completion failed")
+ if i >= 0 {
+ fmt.Fprintf(msg, " at %d", i)
+ }
+ fmt.Fprint(msg, " because of ")
+ fmt.Fprintf(msg, reason, args...)
+ fmt.Fprint(msg, ":\nexpected:\n")
+ for _, d := range want {
+ fmt.Fprintf(msg, " %v\n", d)
+ }
+ fmt.Fprintf(msg, "got:\n")
+ for _, d := range got {
+ fmt.Fprintf(msg, " %v\n", d)
+ }
+ return msg.String()
+}
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 3a96ceb..1ebe8be 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -17,9 +17,14 @@
)
func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
+ uri := params.TextDocument.URI.SpanURI()
+ if !uri.IsFile() {
+ return nil
+ }
+
_, err := s.didModifyFiles(ctx, []source.FileModification{
{
- URI: span.NewURI(params.TextDocument.URI),
+ URI: uri,
Action: source.Open,
Version: params.TextDocument.Version,
Text: []byte(params.TextDocument.Text),
@@ -30,7 +35,11 @@
}
func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
- uri := span.NewURI(params.TextDocument.URI)
+ uri := params.TextDocument.URI.SpanURI()
+ if !uri.IsFile() {
+ return nil
+ }
+
text, err := s.changedText(ctx, uri, params.ContentChanges)
if err != nil {
return err
@@ -64,27 +73,56 @@
func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error {
var modifications []source.FileModification
+ deletions := make(map[span.URI]struct{})
for _, change := range params.Changes {
- uri := span.NewURI(change.URI)
-
- // Do nothing if the file is open in the editor.
- // The editor is the source of truth.
- if s.session.IsOpen(uri) {
+ uri := change.URI.SpanURI()
+ if !uri.IsFile() {
continue
}
+ action := changeTypeToFileAction(change.Type)
modifications = append(modifications, source.FileModification{
URI: uri,
- Action: changeTypeToFileAction(change.Type),
+ Action: action,
OnDisk: true,
})
+ // Keep track of deleted files so that we can clear their diagnostics.
+ // A file might be re-created after deletion, so only mark files that
+ // have truly been deleted.
+ switch action {
+ case source.Delete:
+ deletions[uri] = struct{}{}
+ case source.Close:
+ default:
+ delete(deletions, uri)
+ }
}
- _, err := s.didModifyFiles(ctx, modifications)
- return err
+ snapshots, err := s.didModifyFiles(ctx, modifications)
+ if err != nil {
+ return err
+ }
+ // Clear the diagnostics for any deleted files.
+ for uri := range deletions {
+ if snapshot := snapshots[uri]; snapshot == nil || snapshot.IsOpen(uri) {
+ continue
+ }
+ if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
+ URI: protocol.URIFromSpanURI(uri),
+ Diagnostics: []protocol.Diagnostic{},
+ Version: 0,
+ }); err != nil {
+ return err
+ }
+ }
+ return nil
}
func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
+ uri := params.TextDocument.URI.SpanURI()
+ if !uri.IsFile() {
+ return nil
+ }
c := source.FileModification{
- URI: span.NewURI(params.TextDocument.URI),
+ URI: uri,
Action: source.Save,
Version: params.TextDocument.Version,
}
@@ -96,15 +134,38 @@
}
func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
- _, err := s.didModifyFiles(ctx, []source.FileModification{
+ uri := params.TextDocument.URI.SpanURI()
+ if !uri.IsFile() {
+ return nil
+ }
+ snapshots, err := s.didModifyFiles(ctx, []source.FileModification{
{
- URI: span.NewURI(params.TextDocument.URI),
+ URI: uri,
Action: source.Close,
Version: -1,
Text: nil,
},
})
- return err
+ if err != nil {
+ return err
+ }
+ snapshot := snapshots[uri]
+ if snapshot == nil {
+ return errors.Errorf("no snapshot for %s", uri)
+ }
+ fh, err := snapshot.GetFile(uri)
+ if err != nil {
+ return err
+ }
+ // If a file has been closed and is not on disk, clear its diagnostics.
+ if _, _, err := fh.Read(ctx); err != nil {
+ return s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
+ URI: protocol.URIFromSpanURI(uri),
+ Diagnostics: []protocol.Diagnostic{},
+ Version: 0,
+ })
+ }
+ return nil
}
func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification) (map[span.URI]source.Snapshot, error) {
@@ -147,11 +208,15 @@
if err != nil {
return nil, err
}
- // If a modification comes in for a go.mod file,
- // and the view was never properly initialized,
- // try to recreate the associated view.
+ // If a modification comes in for the view's go.mod file and the view
+ // was never properly initialized, or the view does not have
+ // a go.mod file, try to recreate the associated view.
switch fh.Identity().Kind {
case source.Mod:
+ modfile, _ := snapshot.View().ModFiles()
+ if modfile != "" || fh.Identity().URI != modfile {
+ continue
+ }
newSnapshot, err := snapshot.View().Rebuild(ctx)
if err != nil {
return nil, err
diff --git a/internal/lsp/workspace_symbol.go b/internal/lsp/workspace_symbol.go
new file mode 100644
index 0000000..c752b7f
--- /dev/null
+++ b/internal/lsp/workspace_symbol.go
@@ -0,0 +1,20 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package lsp
+
+import (
+ "context"
+
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/telemetry/trace"
+)
+
+func (s *Server) symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
+ ctx, done := trace.StartSpan(ctx, "lsp.Server.symbol")
+ defer done()
+
+ return source.WorkspaceSymbols(ctx, s.session.Views(), params.Query)
+}
diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go
index 232692c..b05a216 100644
--- a/internal/memoize/memoize.go
+++ b/internal/memoize/memoize.go
@@ -16,6 +16,7 @@
import (
"context"
+ "reflect"
"runtime"
"sync"
"unsafe"
@@ -151,6 +152,18 @@
return (*Handle)(unsafe.Pointer(e))
}
+// Stats returns the number of each type of value in the store.
+func (s *Store) Stats() map[reflect.Type]int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ result := map[reflect.Type]int{}
+ for k := range s.entries {
+ result[reflect.TypeOf(k)]++
+ }
+ return result
+}
+
// Cached returns the value associated with a handle.
//
// It will never cause the value to be generated.
diff --git a/internal/module/module.go b/internal/module/module.go
deleted file mode 100644
index 9a4edb9..0000000
--- a/internal/module/module.go
+++ /dev/null
@@ -1,540 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package module defines the module.Version type
-// along with support code.
-package module
-
-// IMPORTANT NOTE
-//
-// This file essentially defines the set of valid import paths for the go command.
-// There are many subtle considerations, including Unicode ambiguity,
-// security, network, and file system representations.
-//
-// This file also defines the set of valid module path and version combinations,
-// another topic with many subtle considerations.
-//
-// Changes to the semantics in this file require approval from rsc.
-
-import (
- "fmt"
- "sort"
- "strings"
- "unicode"
- "unicode/utf8"
-
- "golang.org/x/tools/internal/semver"
-)
-
-// A Version is defined by a module path and version pair.
-type Version struct {
- Path string
-
- // Version is usually a semantic version in canonical form.
- // There are two exceptions to this general rule.
- // First, the top-level target of a build has no specific version
- // and uses Version = "".
- // Second, during MVS calculations the version "none" is used
- // to represent the decision to take no version of a given module.
- Version string `json:",omitempty"`
-}
-
-// Check checks that a given module path, version pair is valid.
-// In addition to the path being a valid module path
-// and the version being a valid semantic version,
-// the two must correspond.
-// For example, the path "yaml/v2" only corresponds to
-// semantic versions beginning with "v2.".
-func Check(path, version string) error {
- if err := CheckPath(path); err != nil {
- return err
- }
- if !semver.IsValid(version) {
- return fmt.Errorf("malformed semantic version %v", version)
- }
- _, pathMajor, _ := SplitPathVersion(path)
- if !MatchPathMajor(version, pathMajor) {
- if pathMajor == "" {
- pathMajor = "v0 or v1"
- }
- if pathMajor[0] == '.' { // .v1
- pathMajor = pathMajor[1:]
- }
- return fmt.Errorf("mismatched module path %v and version %v (want %v)", path, version, pathMajor)
- }
- return nil
-}
-
-// firstPathOK reports whether r can appear in the first element of a module path.
-// The first element of the path must be an LDH domain name, at least for now.
-// To avoid case ambiguity, the domain name must be entirely lower case.
-func firstPathOK(r rune) bool {
- return r == '-' || r == '.' ||
- '0' <= r && r <= '9' ||
- 'a' <= r && r <= 'z'
-}
-
-// pathOK reports whether r can appear in an import path element.
-// Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: + - . _ and ~.
-// This matches what "go get" has historically recognized in import paths.
-// TODO(rsc): We would like to allow Unicode letters, but that requires additional
-// care in the safe encoding (see note below).
-func pathOK(r rune) bool {
- if r < utf8.RuneSelf {
- return r == '+' || r == '-' || r == '.' || r == '_' || r == '~' ||
- '0' <= r && r <= '9' ||
- 'A' <= r && r <= 'Z' ||
- 'a' <= r && r <= 'z'
- }
- return false
-}
-
-// fileNameOK reports whether r can appear in a file name.
-// For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters.
-// If we expand the set of allowed characters here, we have to
-// work harder at detecting potential case-folding and normalization collisions.
-// See note about "safe encoding" below.
-func fileNameOK(r rune) bool {
- if r < utf8.RuneSelf {
- // Entire set of ASCII punctuation, from which we remove characters:
- // ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
- // We disallow some shell special characters: " ' * < > ? ` |
- // (Note that some of those are disallowed by the Windows file system as well.)
- // We also disallow path separators / : and \ (fileNameOK is only called on path element characters).
- // We allow spaces (U+0020) in file names.
- const allowed = "!#$%&()+,-.=@[]^_{}~ "
- if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' {
- return true
- }
- for i := 0; i < len(allowed); i++ {
- if rune(allowed[i]) == r {
- return true
- }
- }
- return false
- }
- // It may be OK to add more ASCII punctuation here, but only carefully.
- // For example Windows disallows < > \, and macOS disallows :, so we must not allow those.
- return unicode.IsLetter(r)
-}
-
-// CheckPath checks that a module path is valid.
-func CheckPath(path string) error {
- if err := checkPath(path, false); err != nil {
- return fmt.Errorf("malformed module path %q: %v", path, err)
- }
- i := strings.Index(path, "/")
- if i < 0 {
- i = len(path)
- }
- if i == 0 {
- return fmt.Errorf("malformed module path %q: leading slash", path)
- }
- if !strings.Contains(path[:i], ".") {
- return fmt.Errorf("malformed module path %q: missing dot in first path element", path)
- }
- if path[0] == '-' {
- return fmt.Errorf("malformed module path %q: leading dash in first path element", path)
- }
- for _, r := range path[:i] {
- if !firstPathOK(r) {
- return fmt.Errorf("malformed module path %q: invalid char %q in first path element", path, r)
- }
- }
- if _, _, ok := SplitPathVersion(path); !ok {
- return fmt.Errorf("malformed module path %q: invalid version", path)
- }
- return nil
-}
-
-// CheckImportPath checks that an import path is valid.
-func CheckImportPath(path string) error {
- if err := checkPath(path, false); err != nil {
- return fmt.Errorf("malformed import path %q: %v", path, err)
- }
- return nil
-}
-
-// checkPath checks that a general path is valid.
-// It returns an error describing why but not mentioning path.
-// Because these checks apply to both module paths and import paths,
-// the caller is expected to add the "malformed ___ path %q: " prefix.
-// fileName indicates whether the final element of the path is a file name
-// (as opposed to a directory name).
-func checkPath(path string, fileName bool) error {
- if !utf8.ValidString(path) {
- return fmt.Errorf("invalid UTF-8")
- }
- if path == "" {
- return fmt.Errorf("empty string")
- }
- if strings.Contains(path, "..") {
- return fmt.Errorf("double dot")
- }
- if strings.Contains(path, "//") {
- return fmt.Errorf("double slash")
- }
- if path[len(path)-1] == '/' {
- return fmt.Errorf("trailing slash")
- }
- elemStart := 0
- for i, r := range path {
- if r == '/' {
- if err := checkElem(path[elemStart:i], fileName); err != nil {
- return err
- }
- elemStart = i + 1
- }
- }
- if err := checkElem(path[elemStart:], fileName); err != nil {
- return err
- }
- return nil
-}
-
-// checkElem checks whether an individual path element is valid.
-// fileName indicates whether the element is a file name (not a directory name).
-func checkElem(elem string, fileName bool) error {
- if elem == "" {
- return fmt.Errorf("empty path element")
- }
- if strings.Count(elem, ".") == len(elem) {
- return fmt.Errorf("invalid path element %q", elem)
- }
- if elem[0] == '.' && !fileName {
- return fmt.Errorf("leading dot in path element")
- }
- if elem[len(elem)-1] == '.' {
- return fmt.Errorf("trailing dot in path element")
- }
- charOK := pathOK
- if fileName {
- charOK = fileNameOK
- }
- for _, r := range elem {
- if !charOK(r) {
- return fmt.Errorf("invalid char %q", r)
- }
- }
-
- // Windows disallows a bunch of path elements, sadly.
- // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
- short := elem
- if i := strings.Index(short, "."); i >= 0 {
- short = short[:i]
- }
- for _, bad := range badWindowsNames {
- if strings.EqualFold(bad, short) {
- return fmt.Errorf("disallowed path element %q", elem)
- }
- }
- return nil
-}
-
-// CheckFilePath checks whether a slash-separated file path is valid.
-func CheckFilePath(path string) error {
- if err := checkPath(path, true); err != nil {
- return fmt.Errorf("malformed file path %q: %v", path, err)
- }
- return nil
-}
-
-// badWindowsNames are the reserved file path elements on Windows.
-// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
-var badWindowsNames = []string{
- "CON",
- "PRN",
- "AUX",
- "NUL",
- "COM1",
- "COM2",
- "COM3",
- "COM4",
- "COM5",
- "COM6",
- "COM7",
- "COM8",
- "COM9",
- "LPT1",
- "LPT2",
- "LPT3",
- "LPT4",
- "LPT5",
- "LPT6",
- "LPT7",
- "LPT8",
- "LPT9",
-}
-
-// SplitPathVersion returns prefix and major version such that prefix+pathMajor == path
-// and version is either empty or "/vN" for N >= 2.
-// As a special case, gopkg.in paths are recognized directly;
-// they require ".vN" instead of "/vN", and for all N, not just N >= 2.
-func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) {
- if strings.HasPrefix(path, "gopkg.in/") {
- return splitGopkgIn(path)
- }
-
- i := len(path)
- dot := false
- for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') {
- if path[i-1] == '.' {
- dot = true
- }
- i--
- }
- if i <= 1 || i == len(path) || path[i-1] != 'v' || path[i-2] != '/' {
- return path, "", true
- }
- prefix, pathMajor = path[:i-2], path[i-2:]
- if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" {
- return path, "", false
- }
- return prefix, pathMajor, true
-}
-
-// splitGopkgIn is like SplitPathVersion but only for gopkg.in paths.
-func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) {
- if !strings.HasPrefix(path, "gopkg.in/") {
- return path, "", false
- }
- i := len(path)
- if strings.HasSuffix(path, "-unstable") {
- i -= len("-unstable")
- }
- for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') {
- i--
- }
- if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' {
- // All gopkg.in paths must end in vN for some N.
- return path, "", false
- }
- prefix, pathMajor = path[:i-2], path[i-2:]
- if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" {
- return path, "", false
- }
- return prefix, pathMajor, true
-}
-
-// MatchPathMajor reports whether the semantic version v
-// matches the path major version pathMajor.
-func MatchPathMajor(v, pathMajor string) bool {
- if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
- pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
- }
- if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" {
- // Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1.
- // For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405.
- return true
- }
- m := semver.Major(v)
- if pathMajor == "" {
- return m == "v0" || m == "v1" || semver.Build(v) == "+incompatible"
- }
- return (pathMajor[0] == '/' || pathMajor[0] == '.') && m == pathMajor[1:]
-}
-
-// CanonicalVersion returns the canonical form of the version string v.
-// It is the same as semver.Canonical(v) except that it preserves the special build suffix "+incompatible".
-func CanonicalVersion(v string) string {
- cv := semver.Canonical(v)
- if semver.Build(v) == "+incompatible" {
- cv += "+incompatible"
- }
- return cv
-}
-
-// Sort sorts the list by Path, breaking ties by comparing Versions.
-func Sort(list []Version) {
- sort.Slice(list, func(i, j int) bool {
- mi := list[i]
- mj := list[j]
- if mi.Path != mj.Path {
- return mi.Path < mj.Path
- }
- // To help go.sum formatting, allow version/file.
- // Compare semver prefix by semver rules,
- // file by string order.
- vi := mi.Version
- vj := mj.Version
- var fi, fj string
- if k := strings.Index(vi, "/"); k >= 0 {
- vi, fi = vi[:k], vi[k:]
- }
- if k := strings.Index(vj, "/"); k >= 0 {
- vj, fj = vj[:k], vj[k:]
- }
- if vi != vj {
- return semver.Compare(vi, vj) < 0
- }
- return fi < fj
- })
-}
-
-// Safe encodings
-//
-// Module paths appear as substrings of file system paths
-// (in the download cache) and of web server URLs in the proxy protocol.
-// In general we cannot rely on file systems to be case-sensitive,
-// nor can we rely on web servers, since they read from file systems.
-// That is, we cannot rely on the file system to keep rsc.io/QUOTE
-// and rsc.io/quote separate. Windows and macOS don't.
-// Instead, we must never require two different casings of a file path.
-// Because we want the download cache to match the proxy protocol,
-// and because we want the proxy protocol to be possible to serve
-// from a tree of static files (which might be stored on a case-insensitive
-// file system), the proxy protocol must never require two different casings
-// of a URL path either.
-//
-// One possibility would be to make the safe encoding be the lowercase
-// hexadecimal encoding of the actual path bytes. This would avoid ever
-// needing different casings of a file path, but it would be fairly illegible
-// to most programmers when those paths appeared in the file system
-// (including in file paths in compiler errors and stack traces)
-// in web server logs, and so on. Instead, we want a safe encoding that
-// leaves most paths unaltered.
-//
-// The safe encoding is this:
-// replace every uppercase letter with an exclamation mark
-// followed by the letter's lowercase equivalent.
-//
-// For example,
-// github.com/Azure/azure-sdk-for-go -> github.com/!azure/azure-sdk-for-go.
-// github.com/GoogleCloudPlatform/cloudsql-proxy -> github.com/!google!cloud!platform/cloudsql-proxy
-// github.com/Sirupsen/logrus -> github.com/!sirupsen/logrus.
-//
-// Import paths that avoid upper-case letters are left unchanged.
-// Note that because import paths are ASCII-only and avoid various
-// problematic punctuation (like : < and >), the safe encoding is also ASCII-only
-// and avoids the same problematic punctuation.
-//
-// Import paths have never allowed exclamation marks, so there is no
-// need to define how to encode a literal !.
-//
-// Although paths are disallowed from using Unicode (see pathOK above),
-// the eventual plan is to allow Unicode letters as well, to assume that
-// file systems and URLs are Unicode-safe (storing UTF-8), and apply
-// the !-for-uppercase convention. Note however that not all runes that
-// are different but case-fold equivalent are an upper/lower pair.
-// For example, U+004B ('K'), U+006B ('k'), and U+212A ('K' for Kelvin)
-// are considered to case-fold to each other. When we do add Unicode
-// letters, we must not assume that upper/lower are the only case-equivalent pairs.
-// Perhaps the Kelvin symbol would be disallowed entirely, for example.
-// Or perhaps it would encode as "!!k", or perhaps as "(212A)".
-//
-// Also, it would be nice to allow Unicode marks as well as letters,
-// but marks include combining marks, and then we must deal not
-// only with case folding but also normalization: both U+00E9 ('é')
-// and U+0065 U+0301 ('e' followed by combining acute accent)
-// look the same on the page and are treated by some file systems
-// as the same path. If we do allow Unicode marks in paths, there
-// must be some kind of normalization to allow only one canonical
-// encoding of any character used in an import path.
-
-// EncodePath returns the safe encoding of the given module path.
-// It fails if the module path is invalid.
-func EncodePath(path string) (encoding string, err error) {
- if err := CheckPath(path); err != nil {
- return "", err
- }
-
- return encodeString(path)
-}
-
-// EncodeVersion returns the safe encoding of the given module version.
-// Versions are allowed to be in non-semver form but must be valid file names
-// and not contain exclamation marks.
-func EncodeVersion(v string) (encoding string, err error) {
- if err := checkElem(v, true); err != nil || strings.Contains(v, "!") {
- return "", fmt.Errorf("disallowed version string %q", v)
- }
- return encodeString(v)
-}
-
-func encodeString(s string) (encoding string, err error) {
- haveUpper := false
- for _, r := range s {
- if r == '!' || r >= utf8.RuneSelf {
- // This should be disallowed by CheckPath, but diagnose anyway.
- // The correctness of the encoding loop below depends on it.
- return "", fmt.Errorf("internal error: inconsistency in EncodePath")
- }
- if 'A' <= r && r <= 'Z' {
- haveUpper = true
- }
- }
-
- if !haveUpper {
- return s, nil
- }
-
- var buf []byte
- for _, r := range s {
- if 'A' <= r && r <= 'Z' {
- buf = append(buf, '!', byte(r+'a'-'A'))
- } else {
- buf = append(buf, byte(r))
- }
- }
- return string(buf), nil
-}
-
-// DecodePath returns the module path of the given safe encoding.
-// It fails if the encoding is invalid or encodes an invalid path.
-func DecodePath(encoding string) (path string, err error) {
- path, ok := decodeString(encoding)
- if !ok {
- return "", fmt.Errorf("invalid module path encoding %q", encoding)
- }
- if err := CheckPath(path); err != nil {
- return "", fmt.Errorf("invalid module path encoding %q: %v", encoding, err)
- }
- return path, nil
-}
-
-// DecodeVersion returns the version string for the given safe encoding.
-// It fails if the encoding is invalid or encodes an invalid version.
-// Versions are allowed to be in non-semver form but must be valid file names
-// and not contain exclamation marks.
-func DecodeVersion(encoding string) (v string, err error) {
- v, ok := decodeString(encoding)
- if !ok {
- return "", fmt.Errorf("invalid version encoding %q", encoding)
- }
- if err := checkElem(v, true); err != nil {
- return "", fmt.Errorf("disallowed version string %q", v)
- }
- return v, nil
-}
-
-func decodeString(encoding string) (string, bool) {
- var buf []byte
-
- bang := false
- for _, r := range encoding {
- if r >= utf8.RuneSelf {
- return "", false
- }
- if bang {
- bang = false
- if r < 'a' || 'z' < r {
- return "", false
- }
- buf = append(buf, byte(r+'A'-'a'))
- continue
- }
- if r == '!' {
- bang = true
- continue
- }
- if 'A' <= r && r <= 'Z' {
- return "", false
- }
- buf = append(buf, byte(r))
- }
- if bang {
- return "", false
- }
- return string(buf), true
-}
diff --git a/internal/module/module_test.go b/internal/module/module_test.go
deleted file mode 100644
index b40bd03..0000000
--- a/internal/module/module_test.go
+++ /dev/null
@@ -1,319 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package module
-
-import "testing"
-
-var checkTests = []struct {
- path string
- version string
- ok bool
-}{
- {"rsc.io/quote", "0.1.0", false},
- {"rsc io/quote", "v1.0.0", false},
-
- {"github.com/go-yaml/yaml", "v0.8.0", true},
- {"github.com/go-yaml/yaml", "v1.0.0", true},
- {"github.com/go-yaml/yaml", "v2.0.0", false},
- {"github.com/go-yaml/yaml", "v2.1.5", false},
- {"github.com/go-yaml/yaml", "v3.0.0", false},
-
- {"github.com/go-yaml/yaml/v2", "v1.0.0", false},
- {"github.com/go-yaml/yaml/v2", "v2.0.0", true},
- {"github.com/go-yaml/yaml/v2", "v2.1.5", true},
- {"github.com/go-yaml/yaml/v2", "v3.0.0", false},
-
- {"gopkg.in/yaml.v0", "v0.8.0", true},
- {"gopkg.in/yaml.v0", "v1.0.0", false},
- {"gopkg.in/yaml.v0", "v2.0.0", false},
- {"gopkg.in/yaml.v0", "v2.1.5", false},
- {"gopkg.in/yaml.v0", "v3.0.0", false},
-
- {"gopkg.in/yaml.v1", "v0.8.0", false},
- {"gopkg.in/yaml.v1", "v1.0.0", true},
- {"gopkg.in/yaml.v1", "v2.0.0", false},
- {"gopkg.in/yaml.v1", "v2.1.5", false},
- {"gopkg.in/yaml.v1", "v3.0.0", false},
-
- // For gopkg.in, .v1 means v1 only (not v0).
- // But early versions of vgo still generated v0 pseudo-versions for it.
- // Even though now we'd generate those as v1 pseudo-versions,
- // we accept the old pseudo-versions to avoid breaking existing go.mod files.
- // For example gopkg.in/yaml.v2@v2.2.1's go.mod requires check.v1 at a v0 pseudo-version.
- {"gopkg.in/check.v1", "v0.0.0", false},
- {"gopkg.in/check.v1", "v0.0.0-20160102150405-abcdef123456", true},
-
- {"gopkg.in/yaml.v2", "v1.0.0", false},
- {"gopkg.in/yaml.v2", "v2.0.0", true},
- {"gopkg.in/yaml.v2", "v2.1.5", true},
- {"gopkg.in/yaml.v2", "v3.0.0", false},
-
- {"rsc.io/quote", "v17.0.0", false},
- {"rsc.io/quote", "v17.0.0+incompatible", true},
-}
-
-func TestCheck(t *testing.T) {
- for _, tt := range checkTests {
- err := Check(tt.path, tt.version)
- if tt.ok && err != nil {
- t.Errorf("Check(%q, %q) = %v, wanted nil error", tt.path, tt.version, err)
- } else if !tt.ok && err == nil {
- t.Errorf("Check(%q, %q) succeeded, wanted error", tt.path, tt.version)
- }
- }
-}
-
-var checkPathTests = []struct {
- path string
- ok bool
- importOK bool
- fileOK bool
-}{
- {"x.y/z", true, true, true},
- {"x.y", true, true, true},
-
- {"", false, false, false},
- {"x.y/\xFFz", false, false, false},
- {"/x.y/z", false, false, false},
- {"x./z", false, false, false},
- {".x/z", false, false, true},
- {"-x/z", false, true, true},
- {"x..y/z", false, false, false},
- {"x.y/z/../../w", false, false, false},
- {"x.y//z", false, false, false},
- {"x.y/z//w", false, false, false},
- {"x.y/z/", false, false, false},
-
- {"x.y/z/v0", false, true, true},
- {"x.y/z/v1", false, true, true},
- {"x.y/z/v2", true, true, true},
- {"x.y/z/v2.0", false, true, true},
- {"X.y/z", false, true, true},
-
- {"!x.y/z", false, false, true},
- {"_x.y/z", false, true, true},
- {"x.y!/z", false, false, true},
- {"x.y\"/z", false, false, false},
- {"x.y#/z", false, false, true},
- {"x.y$/z", false, false, true},
- {"x.y%/z", false, false, true},
- {"x.y&/z", false, false, true},
- {"x.y'/z", false, false, false},
- {"x.y(/z", false, false, true},
- {"x.y)/z", false, false, true},
- {"x.y*/z", false, false, false},
- {"x.y+/z", false, true, true},
- {"x.y,/z", false, false, true},
- {"x.y-/z", true, true, true},
- {"x.y./zt", false, false, false},
- {"x.y:/z", false, false, false},
- {"x.y;/z", false, false, false},
- {"x.y</z", false, false, false},
- {"x.y=/z", false, false, true},
- {"x.y>/z", false, false, false},
- {"x.y?/z", false, false, false},
- {"x.y@/z", false, false, true},
- {"x.y[/z", false, false, true},
- {"x.y\\/z", false, false, false},
- {"x.y]/z", false, false, true},
- {"x.y^/z", false, false, true},
- {"x.y_/z", false, true, true},
- {"x.y`/z", false, false, false},
- {"x.y{/z", false, false, true},
- {"x.y}/z", false, false, true},
- {"x.y~/z", false, true, true},
- {"x.y/z!", false, false, true},
- {"x.y/z\"", false, false, false},
- {"x.y/z#", false, false, true},
- {"x.y/z$", false, false, true},
- {"x.y/z%", false, false, true},
- {"x.y/z&", false, false, true},
- {"x.y/z'", false, false, false},
- {"x.y/z(", false, false, true},
- {"x.y/z)", false, false, true},
- {"x.y/z*", false, false, false},
- {"x.y/z+", true, true, true},
- {"x.y/z,", false, false, true},
- {"x.y/z-", true, true, true},
- {"x.y/z.t", true, true, true},
- {"x.y/z/t", true, true, true},
- {"x.y/z:", false, false, false},
- {"x.y/z;", false, false, false},
- {"x.y/z<", false, false, false},
- {"x.y/z=", false, false, true},
- {"x.y/z>", false, false, false},
- {"x.y/z?", false, false, false},
- {"x.y/z@", false, false, true},
- {"x.y/z[", false, false, true},
- {"x.y/z\\", false, false, false},
- {"x.y/z]", false, false, true},
- {"x.y/z^", false, false, true},
- {"x.y/z_", true, true, true},
- {"x.y/z`", false, false, false},
- {"x.y/z{", false, false, true},
- {"x.y/z}", false, false, true},
- {"x.y/z~", true, true, true},
- {"x.y/x.foo", true, true, true},
- {"x.y/aux.foo", false, false, false},
- {"x.y/prn", false, false, false},
- {"x.y/prn2", true, true, true},
- {"x.y/com", true, true, true},
- {"x.y/com1", false, false, false},
- {"x.y/com1.txt", false, false, false},
- {"x.y/calm1", true, true, true},
- {"github.com/!123/logrus", false, false, true},
-
- // TODO: CL 41822 allowed Unicode letters in old "go get"
- // without due consideration of the implications, and only on github.com (!).
- // For now, we disallow non-ASCII characters in module mode,
- // in both module paths and general import paths,
- // until we can get the implications right.
- // When we do, we'll enable them everywhere, not just for GitHub.
- {"github.com/user/unicode/испытание", false, false, true},
-
- {"../x", false, false, false},
- {"./y", false, false, false},
- {"x:y", false, false, false},
- {`\temp\foo`, false, false, false},
- {".gitignore", false, false, true},
- {".github/ISSUE_TEMPLATE", false, false, true},
- {"x☺y", false, false, false},
-}
-
-func TestCheckPath(t *testing.T) {
- for _, tt := range checkPathTests {
- err := CheckPath(tt.path)
- if tt.ok && err != nil {
- t.Errorf("CheckPath(%q) = %v, wanted nil error", tt.path, err)
- } else if !tt.ok && err == nil {
- t.Errorf("CheckPath(%q) succeeded, wanted error", tt.path)
- }
-
- err = CheckImportPath(tt.path)
- if tt.importOK && err != nil {
- t.Errorf("CheckImportPath(%q) = %v, wanted nil error", tt.path, err)
- } else if !tt.importOK && err == nil {
- t.Errorf("CheckImportPath(%q) succeeded, wanted error", tt.path)
- }
-
- err = CheckFilePath(tt.path)
- if tt.fileOK && err != nil {
- t.Errorf("CheckFilePath(%q) = %v, wanted nil error", tt.path, err)
- } else if !tt.fileOK && err == nil {
- t.Errorf("CheckFilePath(%q) succeeded, wanted error", tt.path)
- }
- }
-}
-
-var splitPathVersionTests = []struct {
- pathPrefix string
- version string
-}{
- {"x.y/z", ""},
- {"x.y/z", "/v2"},
- {"x.y/z", "/v3"},
- {"x.y/v", ""},
- {"gopkg.in/yaml", ".v0"},
- {"gopkg.in/yaml", ".v1"},
- {"gopkg.in/yaml", ".v2"},
- {"gopkg.in/yaml", ".v3"},
-}
-
-func TestSplitPathVersion(t *testing.T) {
- for _, tt := range splitPathVersionTests {
- pathPrefix, version, ok := SplitPathVersion(tt.pathPrefix + tt.version)
- if pathPrefix != tt.pathPrefix || version != tt.version || !ok {
- t.Errorf("SplitPathVersion(%q) = %q, %q, %v, want %q, %q, true", tt.pathPrefix+tt.version, pathPrefix, version, ok, tt.pathPrefix, tt.version)
- }
- }
-
- for _, tt := range checkPathTests {
- pathPrefix, version, ok := SplitPathVersion(tt.path)
- if pathPrefix+version != tt.path {
- t.Errorf("SplitPathVersion(%q) = %q, %q, %v, doesn't add to input", tt.path, pathPrefix, version, ok)
- }
- }
-}
-
-var encodeTests = []struct {
- path string
- enc string // empty means same as path
-}{
- {path: "ascii.com/abcdefghijklmnopqrstuvwxyz.-+/~_0123456789"},
- {path: "github.com/GoogleCloudPlatform/omega", enc: "github.com/!google!cloud!platform/omega"},
-}
-
-func TestEncodePath(t *testing.T) {
- // Check invalid paths.
- for _, tt := range checkPathTests {
- if !tt.ok {
- _, err := EncodePath(tt.path)
- if err == nil {
- t.Errorf("EncodePath(%q): succeeded, want error (invalid path)", tt.path)
- }
- }
- }
-
- // Check encodings.
- for _, tt := range encodeTests {
- enc, err := EncodePath(tt.path)
- if err != nil {
- t.Errorf("EncodePath(%q): unexpected error: %v", tt.path, err)
- continue
- }
- want := tt.enc
- if want == "" {
- want = tt.path
- }
- if enc != want {
- t.Errorf("EncodePath(%q) = %q, want %q", tt.path, enc, want)
- }
- }
-}
-
-var badDecode = []string{
- "github.com/GoogleCloudPlatform/omega",
- "github.com/!google!cloud!platform!/omega",
- "github.com/!0google!cloud!platform/omega",
- "github.com/!_google!cloud!platform/omega",
- "github.com/!!google!cloud!platform/omega",
- "",
-}
-
-func TestDecodePath(t *testing.T) {
- // Check invalid decodings.
- for _, bad := range badDecode {
- _, err := DecodePath(bad)
- if err == nil {
- t.Errorf("DecodePath(%q): succeeded, want error (invalid decoding)", bad)
- }
- }
-
- // Check invalid paths (or maybe decodings).
- for _, tt := range checkPathTests {
- if !tt.ok {
- path, err := DecodePath(tt.path)
- if err == nil {
- t.Errorf("DecodePath(%q) = %q, want error (invalid path)", tt.path, path)
- }
- }
- }
-
- // Check encodings.
- for _, tt := range encodeTests {
- enc := tt.enc
- if enc == "" {
- enc = tt.path
- }
- path, err := DecodePath(enc)
- if err != nil {
- t.Errorf("DecodePath(%q): unexpected error: %v", enc, err)
- continue
- }
- if path != tt.path {
- t.Errorf("DecodePath(%q) = %q, want %q", enc, path, tt.path)
- }
- }
-}
diff --git a/internal/packagesinternal/packages.go b/internal/packagesinternal/packages.go
index 0c0dbb6..b13ce33 100644
--- a/internal/packagesinternal/packages.go
+++ b/internal/packagesinternal/packages.go
@@ -1,4 +1,27 @@
// Package packagesinternal exposes internal-only fields from go/packages.
package packagesinternal
+import "time"
+
+// Fields must match go list;
+type Module struct {
+ Path string // module path
+ Version string // module version
+ Versions []string // available module versions (with -versions)
+ Replace *Module // replaced by this module
+ Time *time.Time // time version was created
+ Update *Module // available update, if any (with -u)
+ Main bool // is this the main module?
+ Indirect bool // is this module only an indirect dependency of main module?
+ Dir string // directory holding files for this module, if any
+ GoMod string // path to go.mod file used when loading this module, if any
+ GoVersion string // go version used in module
+ Error *ModuleError // error loading module
+}
+type ModuleError struct {
+ Err string // the error itself
+}
+
var GetForTest = func(p interface{}) string { return "" }
+
+var GetModule = func(p interface{}) *Module { return nil }
diff --git a/internal/semver/semver.go b/internal/semver/semver.go
deleted file mode 100644
index 4af7118..0000000
--- a/internal/semver/semver.go
+++ /dev/null
@@ -1,388 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package semver implements comparison of semantic version strings.
-// In this package, semantic version strings must begin with a leading "v",
-// as in "v1.0.0".
-//
-// The general form of a semantic version string accepted by this package is
-//
-// vMAJOR[.MINOR[.PATCH[-PRERELEASE][+BUILD]]]
-//
-// where square brackets indicate optional parts of the syntax;
-// MAJOR, MINOR, and PATCH are decimal integers without extra leading zeros;
-// PRERELEASE and BUILD are each a series of non-empty dot-separated identifiers
-// using only alphanumeric characters and hyphens; and
-// all-numeric PRERELEASE identifiers must not have leading zeros.
-//
-// This package follows Semantic Versioning 2.0.0 (see semver.org)
-// with two exceptions. First, it requires the "v" prefix. Second, it recognizes
-// vMAJOR and vMAJOR.MINOR (with no prerelease or build suffixes)
-// as shorthands for vMAJOR.0.0 and vMAJOR.MINOR.0.
-package semver
-
-// parsed returns the parsed form of a semantic version string.
-type parsed struct {
- major string
- minor string
- patch string
- short string
- prerelease string
- build string
- err string
-}
-
-// IsValid reports whether v is a valid semantic version string.
-func IsValid(v string) bool {
- _, ok := parse(v)
- return ok
-}
-
-// Canonical returns the canonical formatting of the semantic version v.
-// It fills in any missing .MINOR or .PATCH and discards build metadata.
-// Two semantic versions compare equal only if their canonical formattings
-// are identical strings.
-// The canonical invalid semantic version is the empty string.
-func Canonical(v string) string {
- p, ok := parse(v)
- if !ok {
- return ""
- }
- if p.build != "" {
- return v[:len(v)-len(p.build)]
- }
- if p.short != "" {
- return v + p.short
- }
- return v
-}
-
-// Major returns the major version prefix of the semantic version v.
-// For example, Major("v2.1.0") == "v2".
-// If v is an invalid semantic version string, Major returns the empty string.
-func Major(v string) string {
- pv, ok := parse(v)
- if !ok {
- return ""
- }
- return v[:1+len(pv.major)]
-}
-
-// MajorMinor returns the major.minor version prefix of the semantic version v.
-// For example, MajorMinor("v2.1.0") == "v2.1".
-// If v is an invalid semantic version string, MajorMinor returns the empty string.
-func MajorMinor(v string) string {
- pv, ok := parse(v)
- if !ok {
- return ""
- }
- i := 1 + len(pv.major)
- if j := i + 1 + len(pv.minor); j <= len(v) && v[i] == '.' && v[i+1:j] == pv.minor {
- return v[:j]
- }
- return v[:i] + "." + pv.minor
-}
-
-// Prerelease returns the prerelease suffix of the semantic version v.
-// For example, Prerelease("v2.1.0-pre+meta") == "-pre".
-// If v is an invalid semantic version string, Prerelease returns the empty string.
-func Prerelease(v string) string {
- pv, ok := parse(v)
- if !ok {
- return ""
- }
- return pv.prerelease
-}
-
-// Build returns the build suffix of the semantic version v.
-// For example, Build("v2.1.0+meta") == "+meta".
-// If v is an invalid semantic version string, Build returns the empty string.
-func Build(v string) string {
- pv, ok := parse(v)
- if !ok {
- return ""
- }
- return pv.build
-}
-
-// Compare returns an integer comparing two versions according to
-// according to semantic version precedence.
-// The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
-//
-// An invalid semantic version string is considered less than a valid one.
-// All invalid semantic version strings compare equal to each other.
-func Compare(v, w string) int {
- pv, ok1 := parse(v)
- pw, ok2 := parse(w)
- if !ok1 && !ok2 {
- return 0
- }
- if !ok1 {
- return -1
- }
- if !ok2 {
- return +1
- }
- if c := compareInt(pv.major, pw.major); c != 0 {
- return c
- }
- if c := compareInt(pv.minor, pw.minor); c != 0 {
- return c
- }
- if c := compareInt(pv.patch, pw.patch); c != 0 {
- return c
- }
- return comparePrerelease(pv.prerelease, pw.prerelease)
-}
-
-// Max canonicalizes its arguments and then returns the version string
-// that compares greater.
-func Max(v, w string) string {
- v = Canonical(v)
- w = Canonical(w)
- if Compare(v, w) > 0 {
- return v
- }
- return w
-}
-
-func parse(v string) (p parsed, ok bool) {
- if v == "" || v[0] != 'v' {
- p.err = "missing v prefix"
- return
- }
- p.major, v, ok = parseInt(v[1:])
- if !ok {
- p.err = "bad major version"
- return
- }
- if v == "" {
- p.minor = "0"
- p.patch = "0"
- p.short = ".0.0"
- return
- }
- if v[0] != '.' {
- p.err = "bad minor prefix"
- ok = false
- return
- }
- p.minor, v, ok = parseInt(v[1:])
- if !ok {
- p.err = "bad minor version"
- return
- }
- if v == "" {
- p.patch = "0"
- p.short = ".0"
- return
- }
- if v[0] != '.' {
- p.err = "bad patch prefix"
- ok = false
- return
- }
- p.patch, v, ok = parseInt(v[1:])
- if !ok {
- p.err = "bad patch version"
- return
- }
- if len(v) > 0 && v[0] == '-' {
- p.prerelease, v, ok = parsePrerelease(v)
- if !ok {
- p.err = "bad prerelease"
- return
- }
- }
- if len(v) > 0 && v[0] == '+' {
- p.build, v, ok = parseBuild(v)
- if !ok {
- p.err = "bad build"
- return
- }
- }
- if v != "" {
- p.err = "junk on end"
- ok = false
- return
- }
- ok = true
- return
-}
-
-func parseInt(v string) (t, rest string, ok bool) {
- if v == "" {
- return
- }
- if v[0] < '0' || '9' < v[0] {
- return
- }
- i := 1
- for i < len(v) && '0' <= v[i] && v[i] <= '9' {
- i++
- }
- if v[0] == '0' && i != 1 {
- return
- }
- return v[:i], v[i:], true
-}
-
-func parsePrerelease(v string) (t, rest string, ok bool) {
- // "A pre-release version MAY be denoted by appending a hyphen and
- // a series of dot separated identifiers immediately following the patch version.
- // Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
- // Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes."
- if v == "" || v[0] != '-' {
- return
- }
- i := 1
- start := 1
- for i < len(v) && v[i] != '+' {
- if !isIdentChar(v[i]) && v[i] != '.' {
- return
- }
- if v[i] == '.' {
- if start == i || isBadNum(v[start:i]) {
- return
- }
- start = i + 1
- }
- i++
- }
- if start == i || isBadNum(v[start:i]) {
- return
- }
- return v[:i], v[i:], true
-}
-
-func parseBuild(v string) (t, rest string, ok bool) {
- if v == "" || v[0] != '+' {
- return
- }
- i := 1
- start := 1
- for i < len(v) {
- if !isIdentChar(v[i]) {
- return
- }
- if v[i] == '.' {
- if start == i {
- return
- }
- start = i + 1
- }
- i++
- }
- if start == i {
- return
- }
- return v[:i], v[i:], true
-}
-
-func isIdentChar(c byte) bool {
- return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '-'
-}
-
-func isBadNum(v string) bool {
- i := 0
- for i < len(v) && '0' <= v[i] && v[i] <= '9' {
- i++
- }
- return i == len(v) && i > 1 && v[0] == '0'
-}
-
-func isNum(v string) bool {
- i := 0
- for i < len(v) && '0' <= v[i] && v[i] <= '9' {
- i++
- }
- return i == len(v)
-}
-
-func compareInt(x, y string) int {
- if x == y {
- return 0
- }
- if len(x) < len(y) {
- return -1
- }
- if len(x) > len(y) {
- return +1
- }
- if x < y {
- return -1
- } else {
- return +1
- }
-}
-
-func comparePrerelease(x, y string) int {
- // "When major, minor, and patch are equal, a pre-release version has
- // lower precedence than a normal version.
- // Example: 1.0.0-alpha < 1.0.0.
- // Precedence for two pre-release versions with the same major, minor,
- // and patch version MUST be determined by comparing each dot separated
- // identifier from left to right until a difference is found as follows:
- // identifiers consisting of only digits are compared numerically and
- // identifiers with letters or hyphens are compared lexically in ASCII
- // sort order. Numeric identifiers always have lower precedence than
- // non-numeric identifiers. A larger set of pre-release fields has a
- // higher precedence than a smaller set, if all of the preceding
- // identifiers are equal.
- // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta <
- // 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0."
- if x == y {
- return 0
- }
- if x == "" {
- return +1
- }
- if y == "" {
- return -1
- }
- for x != "" && y != "" {
- x = x[1:] // skip - or .
- y = y[1:] // skip - or .
- var dx, dy string
- dx, x = nextIdent(x)
- dy, y = nextIdent(y)
- if dx != dy {
- ix := isNum(dx)
- iy := isNum(dy)
- if ix != iy {
- if ix {
- return -1
- } else {
- return +1
- }
- }
- if ix {
- if len(dx) < len(dy) {
- return -1
- }
- if len(dx) > len(dy) {
- return +1
- }
- }
- if dx < dy {
- return -1
- } else {
- return +1
- }
- }
- }
- if x == "" {
- return -1
- } else {
- return +1
- }
-}
-
-func nextIdent(x string) (dx, rest string) {
- i := 0
- for i < len(x) && x[i] != '.' {
- i++
- }
- return x[:i], x[i:]
-}
diff --git a/internal/semver/semver_test.go b/internal/semver/semver_test.go
deleted file mode 100644
index 96b64a5..0000000
--- a/internal/semver/semver_test.go
+++ /dev/null
@@ -1,182 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package semver
-
-import (
- "strings"
- "testing"
-)
-
-var tests = []struct {
- in string
- out string
-}{
- {"bad", ""},
- {"v1-alpha.beta.gamma", ""},
- {"v1-pre", ""},
- {"v1+meta", ""},
- {"v1-pre+meta", ""},
- {"v1.2-pre", ""},
- {"v1.2+meta", ""},
- {"v1.2-pre+meta", ""},
- {"v1.0.0-alpha", "v1.0.0-alpha"},
- {"v1.0.0-alpha.1", "v1.0.0-alpha.1"},
- {"v1.0.0-alpha.beta", "v1.0.0-alpha.beta"},
- {"v1.0.0-beta", "v1.0.0-beta"},
- {"v1.0.0-beta.2", "v1.0.0-beta.2"},
- {"v1.0.0-beta.11", "v1.0.0-beta.11"},
- {"v1.0.0-rc.1", "v1.0.0-rc.1"},
- {"v1", "v1.0.0"},
- {"v1.0", "v1.0.0"},
- {"v1.0.0", "v1.0.0"},
- {"v1.2", "v1.2.0"},
- {"v1.2.0", "v1.2.0"},
- {"v1.2.3-456", "v1.2.3-456"},
- {"v1.2.3-456.789", "v1.2.3-456.789"},
- {"v1.2.3-456-789", "v1.2.3-456-789"},
- {"v1.2.3-456a", "v1.2.3-456a"},
- {"v1.2.3-pre", "v1.2.3-pre"},
- {"v1.2.3-pre+meta", "v1.2.3-pre"},
- {"v1.2.3-pre.1", "v1.2.3-pre.1"},
- {"v1.2.3-zzz", "v1.2.3-zzz"},
- {"v1.2.3", "v1.2.3"},
- {"v1.2.3+meta", "v1.2.3"},
- {"v1.2.3+meta-pre", "v1.2.3"},
-}
-
-func TestIsValid(t *testing.T) {
- for _, tt := range tests {
- ok := IsValid(tt.in)
- if ok != (tt.out != "") {
- t.Errorf("IsValid(%q) = %v, want %v", tt.in, ok, !ok)
- }
- }
-}
-
-func TestCanonical(t *testing.T) {
- for _, tt := range tests {
- out := Canonical(tt.in)
- if out != tt.out {
- t.Errorf("Canonical(%q) = %q, want %q", tt.in, out, tt.out)
- }
- }
-}
-
-func TestMajor(t *testing.T) {
- for _, tt := range tests {
- out := Major(tt.in)
- want := ""
- if i := strings.Index(tt.out, "."); i >= 0 {
- want = tt.out[:i]
- }
- if out != want {
- t.Errorf("Major(%q) = %q, want %q", tt.in, out, want)
- }
- }
-}
-
-func TestMajorMinor(t *testing.T) {
- for _, tt := range tests {
- out := MajorMinor(tt.in)
- var want string
- if tt.out != "" {
- want = tt.in
- if i := strings.Index(want, "+"); i >= 0 {
- want = want[:i]
- }
- if i := strings.Index(want, "-"); i >= 0 {
- want = want[:i]
- }
- switch strings.Count(want, ".") {
- case 0:
- want += ".0"
- case 1:
- // ok
- case 2:
- want = want[:strings.LastIndex(want, ".")]
- }
- }
- if out != want {
- t.Errorf("MajorMinor(%q) = %q, want %q", tt.in, out, want)
- }
- }
-}
-
-func TestPrerelease(t *testing.T) {
- for _, tt := range tests {
- pre := Prerelease(tt.in)
- var want string
- if tt.out != "" {
- if i := strings.Index(tt.out, "-"); i >= 0 {
- want = tt.out[i:]
- }
- }
- if pre != want {
- t.Errorf("Prerelease(%q) = %q, want %q", tt.in, pre, want)
- }
- }
-}
-
-func TestBuild(t *testing.T) {
- for _, tt := range tests {
- build := Build(tt.in)
- var want string
- if tt.out != "" {
- if i := strings.Index(tt.in, "+"); i >= 0 {
- want = tt.in[i:]
- }
- }
- if build != want {
- t.Errorf("Build(%q) = %q, want %q", tt.in, build, want)
- }
- }
-}
-
-func TestCompare(t *testing.T) {
- for i, ti := range tests {
- for j, tj := range tests {
- cmp := Compare(ti.in, tj.in)
- var want int
- if ti.out == tj.out {
- want = 0
- } else if i < j {
- want = -1
- } else {
- want = +1
- }
- if cmp != want {
- t.Errorf("Compare(%q, %q) = %d, want %d", ti.in, tj.in, cmp, want)
- }
- }
- }
-}
-
-func TestMax(t *testing.T) {
- for i, ti := range tests {
- for j, tj := range tests {
- max := Max(ti.in, tj.in)
- want := Canonical(ti.in)
- if i < j {
- want = Canonical(tj.in)
- }
- if max != want {
- t.Errorf("Max(%q, %q) = %q, want %q", ti.in, tj.in, max, want)
- }
- }
- }
-}
-
-var (
- v1 = "v1.0.0+metadata-dash"
- v2 = "v1.0.0+metadata-dash1"
-)
-
-func BenchmarkCompare(b *testing.B) {
- for i := 0; i < b.N; i++ {
- if Compare(v1, v2) != 0 {
- b.Fatalf("bad compare")
- }
- }
-}
diff --git a/internal/span/parse.go b/internal/span/parse.go
index b3f268a..aa17c84 100644
--- a/internal/span/parse.go
+++ b/internal/span/parse.go
@@ -11,7 +11,7 @@
)
// Parse returns the location represented by the input.
-// All inputs are valid locations, as they can always be a pure filename.
+// Only file paths are accepted, not URIs.
// The returned span will be normalized, and thus if printed may produce a
// different string.
func Parse(input string) Span {
@@ -32,12 +32,12 @@
}
switch {
case suf.sep == ":":
- return New(NewURI(suf.remains), NewPoint(suf.num, hold, offset), Point{})
+ return New(URIFromPath(suf.remains), NewPoint(suf.num, hold, offset), Point{})
case suf.sep == "-":
// we have a span, fall out of the case to continue
default:
// separator not valid, rewind to either the : or the start
- return New(NewURI(valid), NewPoint(hold, 0, offset), Point{})
+ return New(URIFromPath(valid), NewPoint(hold, 0, offset), Point{})
}
// only the span form can get here
// at this point we still don't know what the numbers we have mean
@@ -53,20 +53,20 @@
}
if suf.sep != ":" {
// turns out we don't have a span after all, rewind
- return New(NewURI(valid), end, Point{})
+ return New(URIFromPath(valid), end, Point{})
}
valid = suf.remains
hold = suf.num
suf = rstripSuffix(suf.remains)
if suf.sep != ":" {
// line#offset only
- return New(NewURI(valid), NewPoint(hold, 0, offset), end)
+ return New(URIFromPath(valid), NewPoint(hold, 0, offset), end)
}
// we have a column, so if end only had one number, it is also the column
if !hadCol {
end = NewPoint(suf.num, end.v.Line, end.v.Offset)
}
- return New(NewURI(suf.remains), NewPoint(suf.num, hold, offset), end)
+ return New(URIFromPath(suf.remains), NewPoint(suf.num, hold, offset), end)
}
type suffix struct {
diff --git a/internal/span/span_test.go b/internal/span/span_test.go
index 8212d0c..150ea3f 100644
--- a/internal/span/span_test.go
+++ b/internal/span/span_test.go
@@ -14,8 +14,7 @@
)
var (
- formats = []string{"%v", "%#v", "%+v"}
- tests = [][]string{
+ tests = [][]string{
{"C:/file_a", "C:/file_a", "file:///C:/file_a:1:1#0"},
{"C:/file_b:1:2", "C:/file_b:#1", "file:///C:/file_b:1:2#1"},
{"C:/file_c:1000", "C:/file_c:#9990", "file:///C:/file_c:1000:1#9990"},
@@ -30,7 +29,7 @@
func TestFormat(t *testing.T) {
converter := lines(10)
for _, test := range tests {
- for ti, text := range test {
+ for ti, text := range test[:2] {
spn := span.Parse(text)
if ti <= 1 {
// we can check %v produces the same as the input
diff --git a/internal/span/token.go b/internal/span/token.go
index d0ec03a..1710b77 100644
--- a/internal/span/token.go
+++ b/internal/span/token.go
@@ -75,7 +75,7 @@
if err != nil {
return Span{}, err
}
- s.v.URI = FileURI(startFilename)
+ s.v.URI = URIFromPath(startFilename)
if r.End.IsValid() {
var endFilename string
endFilename, s.v.End.Line, s.v.End.Column, err = position(f, r.End)
diff --git a/internal/span/token_test.go b/internal/span/token_test.go
index db11df1..81b2631 100644
--- a/internal/span/token_test.go
+++ b/internal/span/token_test.go
@@ -32,10 +32,10 @@
}
var tokenTests = []span.Span{
- span.New(span.FileURI("/a.go"), span.NewPoint(1, 1, 0), span.Point{}),
- span.New(span.FileURI("/a.go"), span.NewPoint(3, 7, 20), span.NewPoint(3, 7, 20)),
- span.New(span.FileURI("/b.go"), span.NewPoint(4, 9, 15), span.NewPoint(4, 13, 19)),
- span.New(span.FileURI("/c.go"), span.NewPoint(4, 1, 26), span.Point{}),
+ span.New(span.URIFromPath("/a.go"), span.NewPoint(1, 1, 0), span.Point{}),
+ span.New(span.URIFromPath("/a.go"), span.NewPoint(3, 7, 20), span.NewPoint(3, 7, 20)),
+ span.New(span.URIFromPath("/b.go"), span.NewPoint(4, 9, 15), span.NewPoint(4, 13, 19)),
+ span.New(span.URIFromPath("/c.go"), span.NewPoint(4, 1, 26), span.Point{}),
}
func TestToken(t *testing.T) {
@@ -44,7 +44,7 @@
for _, f := range testdata {
file := fset.AddFile(f.uri, -1, len(f.content))
file.SetLinesForContent(f.content)
- files[span.FileURI(f.uri)] = file
+ files[span.URIFromPath(f.uri)] = file
}
for _, test := range tokenTests {
f := files[test.URI()]
diff --git a/internal/span/uri.go b/internal/span/uri.go
index 26dc90c..f9f7760 100644
--- a/internal/span/uri.go
+++ b/internal/span/uri.go
@@ -20,6 +20,10 @@
// URI represents the full URI for a file.
type URI string
+func (uri URI) IsFile() bool {
+ return strings.HasPrefix(string(uri), "file://")
+}
+
// Filename returns the file path for the given URI.
// It is an error to call this on a URI that is not a valid filename.
func (uri URI) Filename() string {
@@ -49,28 +53,27 @@
return u.Path, nil
}
-// NewURI returns a span URI for the string.
-// It will attempt to detect if the string is a file path or uri.
-func NewURI(s string) URI {
- // If a path has a scheme, it is already a URI.
- // We only handle the file:// scheme.
- if i := len(fileScheme + "://"); strings.HasPrefix(s, "file:///") {
- // Handle microsoft/vscode#75027 by making it a special case.
- // On Windows, VS Code sends file URIs that look like file:///C%3A/x/y/z.
- // Replace the %3A so that the URI looks like: file:///C:/x/y/z.
- if strings.ToLower(s[i+2:i+5]) == "%3a" {
- s = s[:i+2] + ":" + s[i+5:]
- }
- // File URIs from Windows may have lowercase drive letters.
- // Since drive letters are guaranteed to be case insensitive,
- // we change them to uppercase to remain consistent.
- // For example, file:///c:/x/y/z becomes file:///C:/x/y/z.
- if isWindowsDriveURIPath(s[i:]) {
- s = s[:i+1] + strings.ToUpper(string(s[i+1])) + s[i+2:]
- }
+func URIFromURI(s string) URI {
+ if !strings.HasPrefix(s, "file:///") {
return URI(s)
}
- return FileURI(s)
+
+ // Even though the input is a URI, it may not be in canonical form. VS Code
+ // in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize.
+ path, err := url.PathUnescape(s[len("file://"):])
+ if err != nil {
+ panic(err)
+ }
+
+ // File URIs from Windows may have lowercase drive letters.
+ // Since drive letters are guaranteed to be case insensitive,
+ // we change them to uppercase to remain consistent.
+ // For example, file:///c:/x/y/z becomes file:///C:/x/y/z.
+ if isWindowsDriveURIPath(path) {
+ path = path[:1] + strings.ToUpper(string(path[1])) + path[2:]
+ }
+ u := url.URL{Scheme: fileScheme, Path: path}
+ return URI(u.String())
}
func CompareURI(a, b URI) int {
@@ -111,9 +114,9 @@
return os.SameFile(infoa, infob)
}
-// FileURI returns a span URI for the supplied file path.
+// URIFromPath returns a span URI for the supplied file path.
// It will always have the file scheme.
-func FileURI(path string) URI {
+func URIFromPath(path string) URI {
if path == "" {
return ""
}
diff --git a/internal/span/uri_test.go b/internal/span/uri_test.go
index a3754e3..cea74aa 100644
--- a/internal/span/uri_test.go
+++ b/internal/span/uri_test.go
@@ -16,7 +16,7 @@
// include Windows-style URIs and filepaths, but we avoid having OS-specific
// tests by using only forward slashes, assuming that the standard library
// functions filepath.ToSlash and filepath.FromSlash do not need testing.
-func TestURI(t *testing.T) {
+func TestURIFromPath(t *testing.T) {
for _, test := range []struct {
path, wantFile string
wantURI span.URI
@@ -56,25 +56,52 @@
wantFile: `C:/Go/src/bob george/george/george.go`,
wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
},
+ } {
+ got := span.URIFromPath(test.path)
+ if got != test.wantURI {
+ t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI)
+ }
+ gotFilename := got.Filename()
+ if gotFilename != test.wantFile {
+ t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile)
+ }
+ }
+}
+
+func TestURIFromURI(t *testing.T) {
+ for _, test := range []struct {
+ inputURI, wantFile string
+ wantURI span.URI
+ }{
{
- path: `file:///c:/Go/src/bob%20george/george/george.go`,
+ inputURI: `file:///c:/Go/src/bob%20george/george/george.go`,
wantFile: `C:/Go/src/bob george/george/george.go`,
wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
},
{
- path: `file:///C%3A/Go/src/bob%20george/george/george.go`,
+ inputURI: `file:///C%3A/Go/src/bob%20george/george/george.go`,
wantFile: `C:/Go/src/bob george/george/george.go`,
wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
},
{
- path: `file:///path/to/%25p%25ercent%25/per%25cent.go`,
+ inputURI: `file:///path/to/%25p%25ercent%25/per%25cent.go`,
wantFile: `/path/to/%p%ercent%/per%cent.go`,
wantURI: span.URI(`file:///path/to/%25p%25ercent%25/per%25cent.go`),
},
+ {
+ inputURI: `file:///C%3A/`,
+ wantFile: `C:/`,
+ wantURI: span.URI(`file:///C:/`),
+ },
+ {
+ inputURI: `file:///`,
+ wantFile: `/`,
+ wantURI: span.URI(`file:///`),
+ },
} {
- got := span.NewURI(test.path)
+ got := span.URIFromURI(test.inputURI)
if got != test.wantURI {
- t.Errorf("NewURI(%q): got %q, expected %q", test.path, got, test.wantURI)
+ t.Errorf("NewURI(%q): got %q, expected %q", test.inputURI, got, test.wantURI)
}
gotFilename := got.Filename()
if gotFilename != test.wantFile {
diff --git a/internal/span/uri_windows_test.go b/internal/span/uri_windows_test.go
index 1370b19..2a2632e 100644
--- a/internal/span/uri_windows_test.go
+++ b/internal/span/uri_windows_test.go
@@ -16,7 +16,7 @@
// include Windows-style URIs and filepaths, but we avoid having OS-specific
// tests by using only forward slashes, assuming that the standard library
// functions filepath.ToSlash and filepath.FromSlash do not need testing.
-func TestURI(t *testing.T) {
+func TestURIFromPath(t *testing.T) {
for _, test := range []struct {
path, wantFile string
wantURI span.URI
@@ -56,28 +56,56 @@
wantFile: `C:\Go\src\bob george\george\george.go`,
wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
},
+ } {
+ got := span.URIFromPath(test.path)
+ if got != test.wantURI {
+ t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI)
+ }
+ gotFilename := got.Filename()
+ if gotFilename != test.wantFile {
+ t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile)
+ }
+ }
+}
+
+func TestURIFromURI(t *testing.T) {
+ for _, test := range []struct {
+ inputURI, wantFile string
+ wantURI span.URI
+ }{
{
- path: `file:///c:/Go/src/bob%20george/george/george.go`,
+ inputURI: `file:///c:/Go/src/bob%20george/george/george.go`,
wantFile: `C:\Go\src\bob george\george\george.go`,
wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
},
{
- path: `file:///C%3A/Go/src/bob%20george/george/george.go`,
+ inputURI: `file:///C%3A/Go/src/bob%20george/george/george.go`,
wantFile: `C:\Go\src\bob george\george\george.go`,
wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"),
},
{
- path: `file:///c:/path/to/%25p%25ercent%25/per%25cent.go`,
+ inputURI: `file:///c:/path/to/%25p%25ercent%25/per%25cent.go`,
wantFile: `C:\path\to\%p%ercent%\per%cent.go`,
wantURI: span.URI(`file:///C:/path/to/%25p%25ercent%25/per%25cent.go`),
},
+ {
+ inputURI: `file:///C%3A/`,
+ wantFile: `C:\`,
+ wantURI: span.URI(`file:///C:/`),
+ },
+ {
+ inputURI: `file:///`,
+ wantFile: `\`,
+ wantURI: span.URI(`file:///`),
+ },
} {
- got := span.NewURI(test.path)
+ got := span.URIFromURI(test.inputURI)
if got != test.wantURI {
- t.Errorf("ToURI: got %s, expected %s", got, test.wantURI)
+ t.Errorf("NewURI(%q): got %q, expected %q", test.inputURI, got, test.wantURI)
}
- if got.Filename() != test.wantFile {
- t.Errorf("Filename: got %s, expected %s", got.Filename(), test.wantFile)
+ gotFilename := got.Filename()
+ if gotFilename != test.wantFile {
+ t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile)
}
}
}
diff --git a/internal/telemetry/export/export.go b/internal/telemetry/export/export.go
index a48c6f1..dc79458 100644
--- a/internal/telemetry/export/export.go
+++ b/internal/telemetry/export/export.go
@@ -35,15 +35,18 @@
exporter = LogWriter(os.Stderr, true)
)
-func AddExporters(e ...Exporter) {
+func SetExporter(e Exporter) {
exporterMu.Lock()
defer exporterMu.Unlock()
- exporter = Multi(append([]Exporter{exporter}, e...)...)
+ exporter = e
}
func StartSpan(ctx context.Context, span *telemetry.Span, at time.Time) {
exporterMu.Lock()
defer exporterMu.Unlock()
+ if exporter == nil {
+ return
+ }
span.Start = at
exporter.StartSpan(ctx, span)
}
@@ -51,6 +54,9 @@
func FinishSpan(ctx context.Context, span *telemetry.Span, at time.Time) {
exporterMu.Lock()
defer exporterMu.Unlock()
+ if exporter == nil {
+ return
+ }
span.Finish = at
exporter.FinishSpan(ctx, span)
}
@@ -58,6 +64,9 @@
func Tag(ctx context.Context, at time.Time, tags telemetry.TagList) {
exporterMu.Lock()
defer exporterMu.Unlock()
+ if exporter == nil {
+ return
+ }
// If context has a span we need to add the tags to it
span := telemetry.GetSpan(ctx)
if span == nil {
@@ -78,6 +87,9 @@
func Log(ctx context.Context, event telemetry.Event) {
exporterMu.Lock()
defer exporterMu.Unlock()
+ if exporter == nil {
+ return
+ }
// If context has a span we need to add the event to it
span := telemetry.GetSpan(ctx)
if span != nil {
@@ -90,11 +102,17 @@
func Metric(ctx context.Context, data telemetry.MetricData) {
exporterMu.Lock()
defer exporterMu.Unlock()
+ if exporter == nil {
+ return
+ }
exporter.Metric(ctx, data)
}
func Flush() {
exporterMu.Lock()
defer exporterMu.Unlock()
+ if exporter == nil {
+ return
+ }
exporter.Flush()
}
diff --git a/internal/telemetry/export/multi.go b/internal/telemetry/export/multi.go
deleted file mode 100644
index df19f2c..0000000
--- a/internal/telemetry/export/multi.go
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package export
-
-import (
- "context"
-
- "golang.org/x/tools/internal/telemetry"
-)
-
-// Multi returns an exporter that invokes all the exporters given to it in order.
-func Multi(e ...Exporter) Exporter {
- a := make(multi, 0, len(e))
- for _, i := range e {
- if i == nil {
- continue
- }
- if i, ok := i.(multi); ok {
- a = append(a, i...)
- continue
- }
- a = append(a, i)
- }
- return a
-}
-
-type multi []Exporter
-
-func (m multi) StartSpan(ctx context.Context, span *telemetry.Span) {
- for _, o := range m {
- o.StartSpan(ctx, span)
- }
-}
-func (m multi) FinishSpan(ctx context.Context, span *telemetry.Span) {
- for _, o := range m {
- o.FinishSpan(ctx, span)
- }
-}
-func (m multi) Log(ctx context.Context, event telemetry.Event) {
- for _, o := range m {
- o.Log(ctx, event)
- }
-}
-func (m multi) Metric(ctx context.Context, data telemetry.MetricData) {
- for _, o := range m {
- o.Metric(ctx, data)
- }
-}
-func (m multi) Flush() {
- for _, o := range m {
- o.Flush()
- }
-}
diff --git a/internal/telemetry/export/null.go b/internal/telemetry/export/null.go
deleted file mode 100644
index cc01ba7..0000000
--- a/internal/telemetry/export/null.go
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package export
-
-import (
- "context"
-
- "golang.org/x/tools/internal/telemetry"
-)
-
-// Null returns an observer that does nothing.
-func Null() Exporter {
- return null{}
-}
-
-type null struct{}
-
-func (null) StartSpan(context.Context, *telemetry.Span) {}
-func (null) FinishSpan(context.Context, *telemetry.Span) {}
-func (null) Log(context.Context, telemetry.Event) {}
-func (null) Metric(context.Context, telemetry.MetricData) {}
-func (null) Flush() {}
diff --git a/internal/telemetry/export/ocagent/README.md b/internal/telemetry/export/ocagent/README.md
index a76f415..e1a9dc9 100644
--- a/internal/telemetry/export/ocagent/README.md
+++ b/internal/telemetry/export/ocagent/README.md
@@ -79,7 +79,7 @@
Rate: 5 * time.Second,
Client: &http.Client{},
})
- export.AddExporters(exporter)
+ export.SetExporter(exporter)
ctx := context.TODO()
mLatency := stats.Float64("latency", "the latency in milliseconds", "ms")
diff --git a/internal/telemetry/export/ocagent/ocagent.go b/internal/telemetry/export/ocagent/ocagent.go
index 31dacab..8e30a7d 100644
--- a/internal/telemetry/export/ocagent/ocagent.go
+++ b/internal/telemetry/export/ocagent/ocagent.go
@@ -78,7 +78,7 @@
exporter.config.Rate = 2 * time.Second
}
go func() {
- for _ = range time.Tick(exporter.config.Rate) {
+ for range time.Tick(exporter.config.Rate) {
exporter.Flush()
}
}()
@@ -170,7 +170,6 @@
if res.Body != nil {
res.Body.Close()
}
- return
}
func errorInExport(message string, args ...interface{}) {
@@ -191,10 +190,10 @@
func convertSpan(span *telemetry.Span) *wire.Span {
result := &wire.Span{
- TraceId: span.ID.TraceID[:],
- SpanId: span.ID.SpanID[:],
+ TraceID: span.ID.TraceID[:],
+ SpanID: span.ID.SpanID[:],
TraceState: nil, //TODO?
- ParentSpanId: span.ParentID[:],
+ ParentSpanID: span.ParentID[:],
Name: toTruncatableString(span.Name),
Kind: wire.UnspecifiedSpanKind,
StartTime: convertTimestamp(span.Start),
diff --git a/internal/telemetry/export/ocagent/wire/common.go b/internal/telemetry/export/ocagent/wire/common.go
index b53fb81..61dbfcd 100644
--- a/internal/telemetry/export/ocagent/wire/common.go
+++ b/internal/telemetry/export/ocagent/wire/common.go
@@ -55,7 +55,7 @@
type StackTrace struct {
StackFrames *StackFrames `json:"stack_frames,omitempty"`
- StackTraceHashId uint64 `json:"stack_trace_hash_id,omitempty"`
+ StackTraceHashID uint64 `json:"stack_trace_hash_id,omitempty"`
}
type StackFrames struct {
@@ -75,7 +75,7 @@
type Module struct {
Module *TruncatableString `json:"module,omitempty"`
- BuildId *TruncatableString `json:"build_id,omitempty"`
+ BuildID *TruncatableString `json:"build_id,omitempty"`
}
type ProcessIdentifier struct {
diff --git a/internal/telemetry/export/ocagent/wire/trace.go b/internal/telemetry/export/ocagent/wire/trace.go
index fb73743..c1a79a5 100644
--- a/internal/telemetry/export/ocagent/wire/trace.go
+++ b/internal/telemetry/export/ocagent/wire/trace.go
@@ -11,10 +11,10 @@
}
type Span struct {
- TraceId []byte `json:"trace_id,omitempty"`
- SpanId []byte `json:"span_id,omitempty"`
+ TraceID []byte `json:"trace_id,omitempty"`
+ SpanID []byte `json:"span_id,omitempty"`
TraceState *TraceState `json:"tracestate,omitempty"`
- ParentSpanId []byte `json:"parent_span_id,omitempty"`
+ ParentSpanID []byte `json:"parent_span_id,omitempty"`
Name *TruncatableString `json:"name,omitempty"`
Kind SpanKind `json:"kind,omitempty"`
StartTime Timestamp `json:"start_time,omitempty"`
@@ -65,7 +65,7 @@
type MessageEvent struct {
Type MessageEventType `json:"type,omitempty"`
- Id uint64 `json:"id,omitempty"`
+ ID uint64 `json:"id,omitempty"`
UncompressedSize uint64 `json:"uncompressed_size,omitempty"`
CompressedSize uint64 `json:"compressed_size,omitempty"`
}
@@ -91,8 +91,8 @@
}
type Link struct {
- TraceId []byte `json:"trace_id,omitempty"`
- SpanId []byte `json:"span_id,omitempty"`
+ TraceID []byte `json:"trace_id,omitempty"`
+ SpanID []byte `json:"span_id,omitempty"`
Type LinkType `json:"type,omitempty"`
Attributes *Attributes `json:"attributes,omitempty"`
TraceState *TraceState `json:"tracestate,omitempty"`
diff --git a/internal/telemetry/export/prometheus/prometheus.go b/internal/telemetry/export/prometheus/prometheus.go
index ccbdf96..ffbb01d 100644
--- a/internal/telemetry/export/prometheus/prometheus.go
+++ b/internal/telemetry/export/prometheus/prometheus.go
@@ -25,11 +25,6 @@
metrics []telemetry.MetricData
}
-func (e *Exporter) StartSpan(ctx context.Context, span *telemetry.Span) {}
-func (e *Exporter) FinishSpan(ctx context.Context, span *telemetry.Span) {}
-func (e *Exporter) Log(ctx context.Context, event telemetry.Event) {}
-func (e *Exporter) Flush() {}
-
func (e *Exporter) Metric(ctx context.Context, data telemetry.MetricData) {
e.mu.Lock()
defer e.mu.Unlock()
diff --git a/internal/telemetry/log/bench_test.go b/internal/telemetry/log/bench_test.go
index dc77bd6..16e3476 100644
--- a/internal/telemetry/log/bench_test.go
+++ b/internal/telemetry/log/bench_test.go
@@ -6,6 +6,7 @@
"strings"
"testing"
+ "golang.org/x/tools/internal/telemetry/export"
tellog "golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/tag"
)
@@ -20,14 +21,14 @@
return len(b), nil
}
-func A(a int) int {
+func A(ctx context.Context, a int) int {
if a > 0 {
_ = 10 * 12
}
- return B("Called from A")
+ return B(ctx, "Called from A")
}
-func B(b string) int {
+func B(ctx context.Context, b string) int {
b = strings.ToUpper(b)
if len(b) > 1024 {
b = strings.ToLower(b)
@@ -54,16 +55,16 @@
return len(b)
}
-func A_log_stdlib(a int) int {
+func A_log_stdlib(ctx context.Context, a int) int {
if a > 0 {
stdlog.Printf("a > 0 where a=%d", a)
_ = 10 * 12
}
stdlog.Print("calling b")
- return B_log_stdlib("Called from A")
+ return B_log_stdlib(ctx, "Called from A")
}
-func B_log_stdlib(b string) int {
+func B_log_stdlib(ctx context.Context, b string) int {
b = strings.ToUpper(b)
stdlog.Printf("b uppercased, so lowercased where len_b=%d", len(b))
if len(b) > 1024 {
@@ -73,12 +74,15 @@
return len(b)
}
-func BenchmarkNoTracingNoMetricsNoLogging(b *testing.B) {
+var values = []int{0, 10, 20, 100, 1000}
+
+func BenchmarkBaseline(b *testing.B) {
+ ctx := context.Background()
b.ReportAllocs()
- values := []int{0, 10, 20, 100, 1000}
+ b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, value := range values {
- if g := A(value); g <= 0 {
+ if g := A(ctx, value); g <= 0 {
b.Fatalf("Unexpected got g(%d) <= 0", g)
}
}
@@ -86,11 +90,27 @@
}
func BenchmarkLoggingNoExporter(b *testing.B) {
+ ctx := context.Background()
+ export.SetExporter(nil)
b.ReportAllocs()
- values := []int{0, 10, 20, 100, 1000}
+ b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, value := range values {
- if g := A_log(context.TODO(), value); g <= 0 {
+ if g := A_log(ctx, value); g <= 0 {
+ b.Fatalf("Unexpected got g(%d) <= 0", g)
+ }
+ }
+ }
+}
+
+func BenchmarkLogging(b *testing.B) {
+ ctx := context.Background()
+ export.SetExporter(export.LogWriter(new(noopWriter), false))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ for _, value := range values {
+ if g := A_log(ctx, value); g <= 0 {
b.Fatalf("Unexpected got g(%d) <= 0", g)
}
}
@@ -98,11 +118,12 @@
}
func BenchmarkLoggingStdlib(b *testing.B) {
+ ctx := context.Background()
b.ReportAllocs()
- values := []int{0, 10, 20, 100, 1000}
+ b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, value := range values {
- if g := A_log_stdlib(value); g <= 0 {
+ if g := A_log_stdlib(ctx, value); g <= 0 {
b.Fatalf("Unexpected got g(%d) <= 0", g)
}
}
diff --git a/present/link.go b/present/link.go
index 2aead35..52c52c0 100644
--- a/present/link.go
+++ b/present/link.go
@@ -78,19 +78,19 @@
return
}
if urlEnd == end {
- simpleUrl := ""
+ simpleURL := ""
url, err := url.Parse(rawURL)
if err == nil {
// If the URL is http://foo.com, drop the http://
// In other words, render [[http://golang.org]] as:
// <a href="http://golang.org">golang.org</a>
if strings.HasPrefix(rawURL, url.Scheme+"://") {
- simpleUrl = strings.TrimPrefix(rawURL, url.Scheme+"://")
+ simpleURL = strings.TrimPrefix(rawURL, url.Scheme+"://")
} else if strings.HasPrefix(rawURL, url.Scheme+":") {
- simpleUrl = strings.TrimPrefix(rawURL, url.Scheme+":")
+ simpleURL = strings.TrimPrefix(rawURL, url.Scheme+":")
}
}
- return renderLink(rawURL, simpleUrl), end + 2
+ return renderLink(rawURL, simpleURL), end + 2
}
if s[urlEnd:urlEnd+2] != "][" {
return
diff --git a/present/parse.go b/present/parse.go
index dd0f00b..98baec7 100644
--- a/present/parse.go
+++ b/present/parse.go
@@ -402,7 +402,7 @@
}
parser := parsers[args[0]]
if parser == nil {
- return nil, fmt.Errorf("%s:%d: unknown command %q\n", name, lines.line, text)
+ return nil, fmt.Errorf("%s:%d: unknown command %q", name, lines.line, text)
}
t, err := parser(ctx, name, lines.line, text)
if err != nil {