diff --git a/cmd/callgraph/main_test.go b/cmd/callgraph/main_test.go index afcb7a967df..ce634139e68 100644 --- a/cmd/callgraph/main_test.go +++ b/cmd/callgraph/main_test.go @@ -15,7 +15,6 @@ import ( "log" "os" "path/filepath" - "runtime" "strings" "testing" @@ -35,10 +34,6 @@ func init() { } func TestCallgraph(t *testing.T) { - if runtime.GOOS == "windows" && runtime.GOARCH == "arm64" { - t.Skipf("skipping due to suspected file corruption bug on windows/arm64 (https://go.dev/issue/50706)") - } - testenv.NeedsTool(t, "go") gopath, err := filepath.Abs("testdata") diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go index da1c2049538..8ee439b06d0 100644 --- a/cmd/deadcode/deadcode.go +++ b/cmd/deadcode/deadcode.go @@ -26,11 +26,14 @@ import ( "strings" "text/template" + "golang.org/x/telemetry/counter" + "golang.org/x/telemetry/crashmonitor" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/rta" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/aliases" ) //go:embed doc.go @@ -62,6 +65,9 @@ Flags: } func main() { + counter.Open() // Enable telemetry counter writing. + crashmonitor.Start() // Enable crash reporting watchdog. + log.SetPrefix("deadcode: ") log.SetFlags(0) // no time prefix @@ -380,10 +386,10 @@ func prettyName(fn *ssa.Function, qualified bool) string { // method receiver? if recv := fn.Signature.Recv(); recv != nil { t := recv.Type() - if ptr, ok := t.(*types.Pointer); ok { + if ptr, ok := aliases.Unalias(t).(*types.Pointer); ok { t = ptr.Elem() } - buf.WriteString(t.(*types.Named).Obj().Name()) + buf.WriteString(aliases.Unalias(t).(*types.Named).Obj().Name()) buf.WriteByte('.') } diff --git a/cmd/deadcode/doc.go b/cmd/deadcode/doc.go index edc8dfd7bd7..66a150dd19d 100644 --- a/cmd/deadcode/doc.go +++ b/cmd/deadcode/doc.go @@ -63,7 +63,7 @@ With no flags, the command prints the name and location of each dead function in the form of a typical compiler diagnostic, for example: $ deadcode -f='{{range .Funcs}}{{println .Position}}{{end}}' -test ./gopls/... - gopls/internal/lsp/command.go:1206:6: unreachable func: openClientEditor + gopls/internal/protocol/command.go:1206:6: unreachable func: openClientEditor gopls/internal/template/parse.go:414:18: unreachable func: Parsed.WriteNode gopls/internal/template/parse.go:419:18: unreachable func: wrNode.writeNode diff --git a/cmd/stringer/endtoend_test.go b/cmd/stringer/endtoend_test.go index e68b61288f6..2b9afa370c5 100644 --- a/cmd/stringer/endtoend_test.go +++ b/cmd/stringer/endtoend_test.go @@ -13,7 +13,6 @@ import ( "bytes" "flag" "fmt" - "go/build" "io" "os" "path" @@ -50,6 +49,8 @@ func TestMain(m *testing.M) { } func TestEndToEnd(t *testing.T) { + testenv.NeedsTool(t, "go") + stringer := stringerPath(t) // Read the testdata directory. fd, err := os.Open("testdata") @@ -76,8 +77,8 @@ func TestEndToEnd(t *testing.T) { continue } t.Run(name, func(t *testing.T) { - if name == "cgo.go" && !build.Default.CgoEnabled { - t.Skipf("cgo is not enabled for %s", name) + if name == "cgo.go" { + testenv.NeedsTool(t, "cgo") } stringerCompileAndRun(t, t.TempDir(), stringer, typeName(name), name) }) @@ -155,6 +156,8 @@ func TestTags(t *testing.T) { // TestConstValueChange verifies that if a constant value changes and // the stringer code is not regenerated, we'll get a compiler error. func TestConstValueChange(t *testing.T) { + testenv.NeedsTool(t, "go") + stringer := stringerPath(t) dir := t.TempDir() source := filepath.Join(dir, "day.go") diff --git a/copyright/copyright.go b/copyright/copyright.go index c084bd0cda8..b13b56e85f1 100644 --- a/copyright/copyright.go +++ b/copyright/copyright.go @@ -94,7 +94,7 @@ func checkFile(toolsDir, filename string) (bool, error) { return shouldAddCopyright, nil } -// Copied from golang.org/x/tools/gopls/internal/lsp/source/util.go. +// Copied from golang.org/x/tools/gopls/internal/golang/util.go. // Matches cgo generated comment as well as the proposed standard: // // https://golang.org/s/generatedcode diff --git a/go.mod b/go.mod index 8cf0ccc7da7..54b8bd473c5 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,13 @@ go 1.18 require ( github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.14.0 - golang.org/x/net v0.20.0 + golang.org/x/mod v0.15.0 + golang.org/x/net v0.21.0 ) require golang.org/x/sync v0.6.0 + +require ( + golang.org/x/sys v0.17.0 // indirect + golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 +) diff --git a/go.sum b/go.sum index cc5534add2c..373ed3cdd52 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,14 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509 h1:Nr7eTQpQZ/ytesxDJpQgaf0t4sdLnnDtAbmtViTrSUo= +golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509/go.mod h1:ZthVHHkOi8rlMEsfFr3Ie42Ym1NonbFNNRKW3ci0UrU= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index 72b703f7bb4..95db20f4be3 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -15,6 +15,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "sort" "strconv" "strings" @@ -128,6 +129,19 @@ type Testing interface { func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result { r := Run(t, dir, a, patterns...) + // If the immediate caller of RunWithSuggestedFixes is in + // x/tools, we apply stricter checks as required by gopls. + inTools := false + { + var pcs [1]uintptr + n := runtime.Callers(1, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + fr, _ := frames.Next() + if fr.Func != nil && strings.HasPrefix(fr.Func.Name(), "golang.org/x/tools/") { + inTools = true + } + } + // Process each result (package) separately, matching up the suggested // fixes into a diff, which we will compare to the .golden file. We have // to do this per-result in case a file appears in two packages, such as in @@ -145,8 +159,14 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns // Validate edits, prepare the fileEdits map and read the file contents. for _, diag := range act.Diagnostics { - for _, sf := range diag.SuggestedFixes { - for _, edit := range sf.TextEdits { + for _, fix := range diag.SuggestedFixes { + + // Assert that lazy fixes have a Category (#65578, #65087). + if inTools && len(fix.TextEdits) == 0 && diag.Category == "" { + t.Errorf("missing Diagnostic.Category for SuggestedFix without TextEdits (gopls requires the category for the name of the fix command") + } + + for _, edit := range fix.TextEdits { start, end := edit.Pos, edit.End if !end.IsValid() { end = start @@ -175,7 +195,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns if _, ok := fileEdits[file]; !ok { fileEdits[file] = make(map[string][]diff.Edit) } - fileEdits[file][sf.Message] = append(fileEdits[file][sf.Message], diff.Edit{ + fileEdits[file][fix.Message] = append(fileEdits[file][fix.Message], diff.Edit{ Start: file.Offset(start), End: file.Offset(end), New: string(edit.NewText), diff --git a/go/analysis/diagnostic.go b/go/analysis/diagnostic.go index f67c97294b5..c638f275819 100644 --- a/go/analysis/diagnostic.go +++ b/go/analysis/diagnostic.go @@ -31,14 +31,14 @@ type Diagnostic struct { // see https://pkg.go.dev/net/url#URL.ResolveReference. URL string - // SuggestedFixes contains suggested fixes for a diagnostic - // which can be used to perform edits to a file that address - // the diagnostic. - // - // Diagnostics should not contain SuggestedFixes that overlap. - SuggestedFixes []SuggestedFix // optional + // SuggestedFixes is an optional list of fixes to address the + // problem described by the diagnostic, each one representing + // an alternative strategy; at most one may be applied. + SuggestedFixes []SuggestedFix - Related []RelatedInformation // optional + // Related contains optional secondary positions and messages + // related to the primary diagnostic. + Related []RelatedInformation } // RelatedInformation contains information related to a diagnostic. @@ -55,8 +55,7 @@ type RelatedInformation struct { // user can choose to apply to their code. Usually the SuggestedFix is // meant to fix the issue flagged by the diagnostic. // -// TextEdits for a SuggestedFix should not overlap, -// nor contain edits for other packages. +// The TextEdits must not overlap, nor contain edits for other packages. type SuggestedFix struct { // A description for this suggested fix to be shown to a user deciding // whether to accept it. diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go index 9e3fde72bb6..ff14ff58f9c 100644 --- a/go/analysis/internal/analysisflags/flags.go +++ b/go/analysis/internal/analysisflags/flags.go @@ -362,15 +362,24 @@ type JSONSuggestedFix struct { Edits []JSONTextEdit `json:"edits"` } -// A JSONDiagnostic can be used to encode and decode analysis.Diagnostics to and -// from JSON. -// TODO(matloob): Should the JSON diagnostics contain ranges? -// If so, how should they be formatted? +// A JSONDiagnostic describes the JSON schema of an analysis.Diagnostic. +// +// TODO(matloob): include End position if present. type JSONDiagnostic struct { - Category string `json:"category,omitempty"` - Posn string `json:"posn"` - Message string `json:"message"` - SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"` + Category string `json:"category,omitempty"` + Posn string `json:"posn"` // e.g. "file.go:line:column" + Message string `json:"message"` + SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"` + Related []JSONRelatedInformation `json:"related,omitempty"` +} + +// A JSONRelated describes a secondary position and message related to +// a primary diagnostic. +// +// TODO(adonovan): include End position if present. +type JSONRelatedInformation struct { + Posn string `json:"posn"` // e.g. "file.go:line:column" + Message string `json:"message"` } // Add adds the result of analysis 'name' on package 'id'. @@ -401,11 +410,19 @@ func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis. Edits: edits, }) } + var related []JSONRelatedInformation + for _, r := range f.Related { + related = append(related, JSONRelatedInformation{ + Posn: fset.Position(r.Pos).String(), + Message: r.Message, + }) + } jdiag := JSONDiagnostic{ Category: f.Category, Posn: fset.Position(f.Pos).String(), Message: f.Message, SuggestedFixes: fixes, + Related: related, } diagnostics = append(diagnostics, jdiag) } diff --git a/go/analysis/passes/deepequalerrors/deepequalerrors.go b/go/analysis/passes/deepequalerrors/deepequalerrors.go index 1a83bddbcec..5e17bd1ab90 100644 --- a/go/analysis/passes/deepequalerrors/deepequalerrors.go +++ b/go/analysis/passes/deepequalerrors/deepequalerrors.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" ) const Doc = `check for calls of reflect.DeepEqual on error values @@ -101,7 +102,8 @@ func containsError(typ types.Type) bool { return true } } - case *types.Named: + case *types.Named, + *aliases.Alias: return check(t.Underlying()) // We list the remaining valid type kinds for completeness. diff --git a/go/analysis/passes/lostcancel/lostcancel.go b/go/analysis/passes/lostcancel/lostcancel.go index 2bccb675020..bf56a5c06f6 100644 --- a/go/analysis/passes/lostcancel/lostcancel.go +++ b/go/analysis/passes/lostcancel/lostcancel.go @@ -172,7 +172,18 @@ func runFunc(pass *analysis.Pass, node ast.Node) { if ret := lostCancelPath(pass, g, v, stmt, sig); ret != nil { lineno := pass.Fset.Position(stmt.Pos()).Line pass.ReportRangef(stmt, "the %s function is not used on all paths (possible context leak)", v.Name()) - pass.ReportRangef(ret, "this return statement may be reached without using the %s var defined on line %d", v.Name(), lineno) + + pos, end := ret.Pos(), ret.End() + // golang/go#64547: cfg.Block.Return may return a synthetic + // ReturnStmt that overflows the file. + if pass.Fset.File(pos) != pass.Fset.File(end) { + end = pos + } + pass.Report(analysis.Diagnostic{ + Pos: pos, + End: end, + Message: fmt.Sprintf("this return statement may be reached without using the %s var defined on line %d", v.Name(), lineno), + }) } } } diff --git a/go/analysis/passes/nilness/nilness.go b/go/analysis/passes/nilness/nilness.go index c4999f7a9db..5e14c096ab1 100644 --- a/go/analysis/passes/nilness/nilness.go +++ b/go/analysis/passes/nilness/nilness.go @@ -189,6 +189,42 @@ func runFunc(pass *analysis.Pass, fn *ssa.Function) { } } + // In code of the form: + // + // if ptr, ok := x.(*T); ok { ... } else { fsucc } + // + // the fsucc block learns that ptr == nil, + // since that's its zero value. + if If, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If); ok { + // Handle "if ok" and "if !ok" variants. + cond, fsucc := If.Cond, b.Succs[1] + if unop, ok := cond.(*ssa.UnOp); ok && unop.Op == token.NOT { + cond, fsucc = unop.X, b.Succs[0] + } + + // Match pattern: + // t0 = typeassert (pointerlike) + // t1 = extract t0 #0 // ptr + // t2 = extract t0 #1 // ok + // if t2 goto tsucc, fsucc + if extract1, ok := cond.(*ssa.Extract); ok && extract1.Index == 1 { + if assert, ok := extract1.Tuple.(*ssa.TypeAssert); ok && + isNillable(assert.AssertedType) { + for _, pinstr := range *assert.Referrers() { + if extract0, ok := pinstr.(*ssa.Extract); ok && + extract0.Index == 0 && + extract0.Tuple == extract1.Tuple { + for _, d := range b.Dominees() { + if len(d.Preds) == 1 && d == fsucc { + visit(d, append(stack, fact{extract0, isnil})) + } + } + } + } + } + } + } + for _, d := range b.Dominees() { visit(d, stack) } @@ -360,3 +396,23 @@ func (ff facts) negate() facts { } return nn } + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} + +func isNillable(t types.Type) bool { + switch t := typeparams.CoreType(t).(type) { + case *types.Pointer, + *types.Map, + *types.Signature, + *types.Chan, + *types.Interface, + *types.Slice: + return true + case *types.Basic: + return t == types.Typ[types.UnsafePointer] + } + return false +} diff --git a/go/analysis/passes/nilness/testdata/src/a/a.go b/go/analysis/passes/nilness/testdata/src/a/a.go index 0629e08d89e..89bccbe65bd 100644 --- a/go/analysis/passes/nilness/testdata/src/a/a.go +++ b/go/analysis/passes/nilness/testdata/src/a/a.go @@ -216,3 +216,29 @@ func f14() { print(x) } } + +func f15(x any) { + ptr, ok := x.(*int) + if ok { + return + } + println(*ptr) // want "nil dereference in load" +} + +func f16(x any) { + ptr, ok := x.(*int) + if !ok { + println(*ptr) // want "nil dereference in load" + return + } + println(*ptr) +} + +func f18(x any) { + ptr, ok := x.(*int) + if ok { + println(ptr) + // falls through + } + println(*ptr) +} diff --git a/go/analysis/passes/stringintconv/string.go b/go/analysis/passes/stringintconv/string.go index b2591ccff55..005e2e54b7d 100644 --- a/go/analysis/passes/stringintconv/string.go +++ b/go/analysis/passes/stringintconv/string.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -194,16 +195,15 @@ func run(pass *analysis.Pass) (interface{}, error) { func structuralTypes(t types.Type) ([]types.Type, error) { var structuralTypes []types.Type - switch t := t.(type) { - case *types.TypeParam: - terms, err := typeparams.StructuralTerms(t) + if tp, ok := aliases.Unalias(t).(*types.TypeParam); ok { + terms, err := typeparams.StructuralTerms(tp) if err != nil { return nil, err } for _, term := range terms { structuralTypes = append(structuralTypes, term.Type()) } - default: + } else { structuralTypes = append(structuralTypes, t) } return structuralTypes, nil diff --git a/go/buildutil/tags.go b/go/buildutil/tags.go index 7cf523bca48..32c8d1424d2 100644 --- a/go/buildutil/tags.go +++ b/go/buildutil/tags.go @@ -4,17 +4,22 @@ package buildutil -// This logic was copied from stringsFlag from $GOROOT/src/cmd/go/build.go. +// This duplicated logic must be kept in sync with that from go build: +// $GOROOT/src/cmd/go/internal/work/build.go (tagsFlag.Set) +// $GOROOT/src/cmd/go/internal/base/flag.go (StringsFlag.Set) +// $GOROOT/src/cmd/internal/quoted/quoted.go (isSpaceByte, Split) -import "fmt" +import ( + "fmt" + "strings" +) const TagsFlagDoc = "a list of `build tags` to consider satisfied during the build. " + "For more information about build tags, see the description of " + "build constraints in the documentation for the go/build package" // TagsFlag is an implementation of the flag.Value and flag.Getter interfaces that parses -// a flag value in the same manner as go build's -tags flag and -// populates a []string slice. +// a flag value the same as go build's -tags flag and populates a []string slice. // // See $GOROOT/src/go/build/doc.go for description of build tags. // See $GOROOT/src/cmd/go/doc.go for description of 'go build -tags' flag. @@ -25,19 +30,32 @@ const TagsFlagDoc = "a list of `build tags` to consider satisfied during the bui type TagsFlag []string func (v *TagsFlag) Set(s string) error { - var err error - *v, err = splitQuotedFields(s) - if *v == nil { - *v = []string{} + // See $GOROOT/src/cmd/go/internal/work/build.go (tagsFlag.Set) + // For compatibility with Go 1.12 and earlier, allow "-tags='a b c'" or even just "-tags='a'". + if strings.Contains(s, " ") || strings.Contains(s, "'") { + var err error + *v, err = splitQuotedFields(s) + if *v == nil { + *v = []string{} + } + return err + } + + // Starting in Go 1.13, the -tags flag is a comma-separated list of build tags. + *v = []string{} + for _, s := range strings.Split(s, ",") { + if s != "" { + *v = append(*v, s) + } } - return err + return nil } func (v *TagsFlag) Get() interface{} { return *v } func splitQuotedFields(s string) ([]string, error) { - // Split fields allowing '' or "" around elements. - // Quotes further inside the string do not count. + // See $GOROOT/src/cmd/internal/quoted/quoted.go (Split) + // This must remain in sync with that logic. var f []string for len(s) > 0 { for len(s) > 0 && isSpaceByte(s[0]) { @@ -76,5 +94,7 @@ func (v *TagsFlag) String() string { } func isSpaceByte(c byte) bool { + // See $GOROOT/src/cmd/internal/quoted/quoted.go (isSpaceByte, Split) + // This list must remain in sync with that. return c == ' ' || c == '\t' || c == '\n' || c == '\r' } diff --git a/go/buildutil/tags_test.go b/go/buildutil/tags_test.go index f8234314fb3..fb3afbccab7 100644 --- a/go/buildutil/tags_test.go +++ b/go/buildutil/tags_test.go @@ -5,28 +5,124 @@ package buildutil_test import ( + "bytes" "flag" "go/build" + "os/exec" "reflect" + "strings" "testing" "golang.org/x/tools/go/buildutil" + "golang.org/x/tools/internal/testenv" ) func TestTags(t *testing.T) { - f := flag.NewFlagSet("TestTags", flag.PanicOnError) - var ctxt build.Context - f.Var((*buildutil.TagsFlag)(&ctxt.BuildTags), "tags", buildutil.TagsFlagDoc) - f.Parse([]string{"-tags", ` 'one'"two" 'three "four"'`, "rest"}) - - // BuildTags - want := []string{"one", "two", "three \"four\""} - if !reflect.DeepEqual(ctxt.BuildTags, want) { - t.Errorf("BuildTags = %q, want %q", ctxt.BuildTags, want) + + type tagTestCase struct { + tags string + want []string + wantErr bool } - // Args() - if want := []string{"rest"}; !reflect.DeepEqual(f.Args(), want) { - t.Errorf("f.Args() = %q, want %q", f.Args(), want) + for name, tc := range map[string]tagTestCase{ + // Normal valid cases + "empty": { + tags: "", + want: []string{}, + }, + "commas": { + tags: "tag1,tag_2,🐹,tag/3,tag-4", + want: []string{"tag1", "tag_2", "🐹", "tag/3", "tag-4"}, + }, + "delimiters are spaces": { + tags: "a b\tc\rd\ne", + want: []string{"a", "b", "c", "d", "e"}, + }, + "old quote and space form": { + tags: "'a' 'b' 'c'", + want: []string{"a", "b", "c"}, + }, + + // Normal error cases + "unterminated": { + tags: `"missing closing quote`, + want: []string{}, + wantErr: true, + }, + "unterminated single": { + tags: `'missing closing quote`, + want: []string{}, + wantErr: true, + }, + + // Maybe surprising difference for unterminated quotes, no spaces + "unterminated no spaces": { + tags: `"missing_closing_quote`, + want: []string{"\"missing_closing_quote"}, + }, + "unterminated no spaces single": { + tags: `'missing_closing_quote`, + want: []string{}, + wantErr: true, + }, + + // Permitted but not recommended + "delimiters contiguous spaces": { + tags: "a \t\r\n, b \t\r\nc,d\te\tf", + want: []string{"a", ",", "b", "c,d", "e", "f"}, + }, + "quotes and spaces": { + tags: ` 'one'"two" 'three "four"'`, + want: []string{"one", "two", "three \"four\""}, + }, + "quotes single no spaces": { + tags: `'t1','t2',"t3"`, + want: []string{"t1", ",'t2',\"t3\""}, + }, + "quotes double no spaces": { + tags: `"t1","t2","t3"`, + want: []string{`"t1"`, `"t2"`, `"t3"`}, + }, + } { + t.Run(name, func(t *testing.T) { + f := flag.NewFlagSet("TestTags", flag.ContinueOnError) + var ctxt build.Context + f.Var((*buildutil.TagsFlag)(&ctxt.BuildTags), "tags", buildutil.TagsFlagDoc) + + // Normal case valid parsed tags + f.Parse([]string{"-tags", tc.tags, "rest"}) + + // BuildTags + if !reflect.DeepEqual(ctxt.BuildTags, tc.want) { + t.Errorf("Case = %s, BuildTags = %q, want %q", name, ctxt.BuildTags, tc.want) + } + + // Args() + if want := []string{"rest"}; !reflect.DeepEqual(f.Args(), want) { + t.Errorf("Case = %s, f.Args() = %q, want %q", name, f.Args(), want) + } + + // Regression check against base go tooling + cmd := testenv.Command(t, "go", "list", "-f", "{{context.BuildTags}}", "-tags", tc.tags, ".") + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { + t.Logf("stderr:\n%s", ee.Stderr) + } + if !tc.wantErr { + t.Errorf("%v: %v", cmd, err) + } + } else if tc.wantErr { + t.Errorf("Expected failure for %v", cmd) + } else { + wantDescription := strings.Join(tc.want, " ") + output := strings.Trim(strings.TrimSuffix(out.String(), "\n"), "[]") + if output != wantDescription { + t.Errorf("Output = %s, want %s", output, wantDescription) + } + } + }) } } diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go index d0ae0fccf57..72b383dabe7 100644 --- a/go/callgraph/rta/rta.go +++ b/go/callgraph/rta/rta.go @@ -45,6 +45,7 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/compat" ) @@ -416,6 +417,9 @@ func (r *rta) implementations(I *types.Interface) []types.Type { // dynamic type of some interface or reflect.Value. // Adapted from needMethods in go/ssa/builder.go func (r *rta) addRuntimeType(T types.Type, skip bool) { + // Never record aliases. + T = aliases.Unalias(T) + if prev, ok := r.result.RuntimeTypes.At(T).(bool); ok { if skip && !prev { r.result.RuntimeTypes.Set(T, skip) @@ -457,7 +461,7 @@ func (r *rta) addRuntimeType(T types.Type, skip bool) { case *types.Named: n = T case *types.Pointer: - n, _ = T.Elem().(*types.Named) + n, _ = aliases.Unalias(T.Elem()).(*types.Named) } if n != nil { owner := n.Obj().Pkg() @@ -476,6 +480,9 @@ func (r *rta) addRuntimeType(T types.Type, skip bool) { } switch t := T.(type) { + case *aliases.Alias: + panic("unreachable") + case *types.Basic: // nop diff --git a/go/callgraph/rta/rta_test.go b/go/callgraph/rta/rta_test.go index a855dd6d369..8552dc7b13c 100644 --- a/go/callgraph/rta/rta_test.go +++ b/go/callgraph/rta/rta_test.go @@ -23,6 +23,7 @@ import ( "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/aliases" ) // TestRTA runs RTA on each testdata/*.go file and compares the @@ -200,7 +201,7 @@ func check(t *testing.T, f *ast.File, pkg *ssa.Package, res *rta.Result) { got := make(stringset) res.RuntimeTypes.Iterate(func(key types.Type, value interface{}) { if !value.(bool) { // accessible to reflection - typ := types.TypeString(key, types.RelativeTo(pkg.Pkg)) + typ := types.TypeString(aliases.Unalias(key), types.RelativeTo(pkg.Pkg)) got[typ] = true } }) diff --git a/go/cfg/cfg.go b/go/cfg/cfg.go index 37d799f4bc3..e9c48d51daa 100644 --- a/go/cfg/cfg.go +++ b/go/cfg/cfg.go @@ -113,7 +113,11 @@ func (b *Block) String() string { return fmt.Sprintf("block %d (%s)", b.Index, b.comment) } -// Return returns the return statement at the end of this block if present, nil otherwise. +// Return returns the return statement at the end of this block if present, nil +// otherwise. +// +// When control falls off the end of the function, the ReturnStmt is synthetic +// and its [ast.Node.End] position may be beyond the end of the file. func (b *Block) Return() (ret *ast.ReturnStmt) { if len(b.Nodes) > 0 { ret, _ = b.Nodes[len(b.Nodes)-1].(*ast.ReturnStmt) diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go index 9fdb6f8b059..72bbe793c1e 100644 --- a/go/internal/gccgoimporter/parser.go +++ b/go/internal/gccgoimporter/parser.go @@ -950,6 +950,7 @@ const ( gccgoBuiltinERROR = 19 gccgoBuiltinBYTE = 20 gccgoBuiltinRUNE = 21 + gccgoBuiltinANY = 22 ) func lookupBuiltinType(typ int) types.Type { @@ -974,6 +975,7 @@ func lookupBuiltinType(typ int) types.Type { gccgoBuiltinERROR: types.Universe.Lookup("error").Type(), gccgoBuiltinBYTE: types.Universe.Lookup("byte").Type(), gccgoBuiltinRUNE: types.Universe.Lookup("rune").Type(), + gccgoBuiltinANY: types.Universe.Lookup("any").Type(), }[typ] } diff --git a/go/loader/loader_test.go b/go/loader/loader_test.go index cab2217c3e2..1e0b16e7fc3 100644 --- a/go/loader/loader_test.go +++ b/go/loader/loader_test.go @@ -837,6 +837,7 @@ func loadIO(t *testing.T) { func TestCgoCwdIssue46877(t *testing.T) { testenv.NeedsTool(t, "go") + testenv.NeedsTool(t, "cgo") var conf loader.Config conf.Import("golang.org/x/tools/go/loader/testdata/issue46877") if _, err := conf.Load(); err != nil { diff --git a/go/loader/stdlib_test.go b/go/loader/stdlib_test.go index 83d70dabdca..ef51325e9c8 100644 --- a/go/loader/stdlib_test.go +++ b/go/loader/stdlib_test.go @@ -130,13 +130,11 @@ func TestCgoOption(t *testing.T) { case "darwin": t.Skipf("golang/go#58493: file locations in this test are stale on darwin") } + testenv.NeedsTool(t, "go") // In nocgo builds (e.g. linux-amd64-nocgo), // there is no "runtime/cgo" package, // so cgo-generated Go files will have a failing import. - if !build.Default.CgoEnabled { - return - } - testenv.NeedsTool(t, "go") + testenv.NeedsTool(t, "cgo") // Test that we can load cgo-using packages with // CGO_ENABLED=[01], which causes go/build to select pure diff --git a/go/packages/doc.go b/go/packages/doc.go index b2a0b7c6a67..a8d7b06ac09 100644 --- a/go/packages/doc.go +++ b/go/packages/doc.go @@ -15,22 +15,10 @@ Load passes most patterns directly to the underlying build tool. The default build tool is the go command. Its supported patterns are described at https://pkg.go.dev/cmd/go#hdr-Package_lists_and_patterns. +Other build systems may be supported by providing a "driver"; +see [The driver protocol]. -Load may be used in Go projects that use alternative build systems, by -installing an appropriate "driver" program for the build system and -specifying its location in the GOPACKAGESDRIVER environment variable. -For example, -https://github.com/bazelbuild/rules_go/wiki/Editor-and-tool-integration -explains how to use the driver for Bazel. -The driver program is responsible for interpreting patterns in its -preferred notation and reporting information about the packages that -they identify. -(See driverRequest and driverResponse types for the JSON -schema used by the protocol. -Though the protocol is supported, these types are currently unexported; -see #64608 for a proposal to publish them.) - -Regardless of driver, all patterns with the prefix "query=", where query is a +All patterns with the prefix "query=", where query is a non-empty string of letters from [a-z], are reserved and may be interpreted as query operators. @@ -86,7 +74,29 @@ for details. Most tools should pass their command-line arguments (after any flags) uninterpreted to [Load], so that it can interpret them according to the conventions of the underlying build system. + See the Example function for typical usage. + +# The driver protocol + +[Load] may be used to load Go packages even in Go projects that use +alternative build systems, by installing an appropriate "driver" +program for the build system and specifying its location in the +GOPACKAGESDRIVER environment variable. +For example, +https://github.com/bazelbuild/rules_go/wiki/Editor-and-tool-integration +explains how to use the driver for Bazel. + +The driver program is responsible for interpreting patterns in its +preferred notation and reporting information about the packages that +those patterns identify. Drivers must also support the special "file=" +and "pattern=" patterns described above. + +The patterns are provided as positional command-line arguments. A +JSON-encoded [DriverRequest] message providing additional information +is written to the driver's standard input. The driver must write a +JSON-encoded [DriverResponse] message to its standard output. (This +message differs from the JSON schema produced by 'go list'.) */ package packages // import "golang.org/x/tools/go/packages" diff --git a/go/packages/external.go b/go/packages/external.go index 7db1d1293ab..4335c1eb14c 100644 --- a/go/packages/external.go +++ b/go/packages/external.go @@ -2,12 +2,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file enables an external tool to intercept package requests. -// If the tool is present then its results are used in preference to -// the go list command. - package packages +// This file defines the protocol that enables an external "driver" +// tool to supply package metadata in place of 'go list'. + import ( "bytes" "encoding/json" @@ -17,31 +16,71 @@ import ( "strings" ) -// The Driver Protocol +// DriverRequest defines the schema of a request for package metadata +// from an external driver program. The JSON-encoded DriverRequest +// message is provided to the driver program's standard input. The +// query patterns are provided as command-line arguments. // -// The driver, given the inputs to a call to Load, returns metadata about the packages specified. -// This allows for different build systems to support go/packages by telling go/packages how the -// packages' source is organized. -// The driver is a binary, either specified by the GOPACKAGESDRIVER environment variable or in -// the path as gopackagesdriver. It's given the inputs to load in its argv. See the package -// documentation in doc.go for the full description of the patterns that need to be supported. -// A driver receives as a JSON-serialized driverRequest struct in standard input and will -// produce a JSON-serialized driverResponse (see definition in packages.go) in its standard output. - -// driverRequest is used to provide the portion of Load's Config that is needed by a driver. -type driverRequest struct { +// See the package documentation for an overview. +type DriverRequest struct { Mode LoadMode `json:"mode"` + // Env specifies the environment the underlying build system should be run in. Env []string `json:"env"` + // BuildFlags are flags that should be passed to the underlying build system. BuildFlags []string `json:"build_flags"` + // Tests specifies whether the patterns should also return test packages. Tests bool `json:"tests"` + // Overlay maps file paths (relative to the driver's working directory) to the byte contents // of overlay files. Overlay map[string][]byte `json:"overlay"` } +// DriverResponse defines the schema of a response from an external +// driver program, providing the results of a query for package +// metadata. The driver program must write a JSON-encoded +// DriverResponse message to its standard output. +// +// See the package documentation for an overview. +type DriverResponse struct { + // NotHandled is returned if the request can't be handled by the current + // driver. If an external driver returns a response with NotHandled, the + // rest of the DriverResponse is ignored, and go/packages will fallback + // to the next driver. If go/packages is extended in the future to support + // lists of multiple drivers, go/packages will fall back to the next driver. + NotHandled bool + + // Compiler and Arch are the arguments pass of types.SizesFor + // to get a types.Sizes to use when type checking. + Compiler string + Arch string + + // Roots is the set of package IDs that make up the root packages. + // We have to encode this separately because when we encode a single package + // we cannot know if it is one of the roots as that requires knowledge of the + // graph it is part of. + Roots []string `json:",omitempty"` + + // Packages is the full set of packages in the graph. + // The packages are not connected into a graph. + // The Imports if populated will be stubs that only have their ID set. + // Imports will be connected and then type and syntax information added in a + // later pass (see refine). + Packages []*Package + + // GoVersion is the minor version number used by the driver + // (e.g. the go command on the PATH) when selecting .go files. + // Zero means unknown. + GoVersion int +} + +// driver is the type for functions that query the build system for the +// packages named by the patterns. +type driver func(cfg *Config, patterns ...string) (*DriverResponse, error) + // findExternalDriver returns the file path of a tool that supplies // the build system package structure, or "" if not found." // If GOPACKAGESDRIVER is set in the environment findExternalTool returns its @@ -64,8 +103,8 @@ func findExternalDriver(cfg *Config) driver { return nil } } - return func(cfg *Config, words ...string) (*driverResponse, error) { - req, err := json.Marshal(driverRequest{ + return func(cfg *Config, words ...string) (*DriverResponse, error) { + req, err := json.Marshal(DriverRequest{ Mode: cfg.Mode, Env: cfg.Env, BuildFlags: cfg.BuildFlags, @@ -92,7 +131,7 @@ func findExternalDriver(cfg *Config) driver { fmt.Fprintf(os.Stderr, "%s stderr: <<%s>>\n", cmdDebugStr(cmd), stderr) } - var response driverResponse + var response DriverResponse if err := json.Unmarshal(buf.Bytes(), &response); err != nil { return nil, err } diff --git a/go/packages/golist.go b/go/packages/golist.go index cd375fbc3c2..22305d9c90a 100644 --- a/go/packages/golist.go +++ b/go/packages/golist.go @@ -35,23 +35,23 @@ type goTooOldError struct { error } -// responseDeduper wraps a driverResponse, deduplicating its contents. +// responseDeduper wraps a DriverResponse, deduplicating its contents. type responseDeduper struct { seenRoots map[string]bool seenPackages map[string]*Package - dr *driverResponse + dr *DriverResponse } func newDeduper() *responseDeduper { return &responseDeduper{ - dr: &driverResponse{}, + dr: &DriverResponse{}, seenRoots: map[string]bool{}, seenPackages: map[string]*Package{}, } } -// addAll fills in r with a driverResponse. -func (r *responseDeduper) addAll(dr *driverResponse) { +// addAll fills in r with a DriverResponse. +func (r *responseDeduper) addAll(dr *DriverResponse) { for _, pkg := range dr.Packages { r.addPackage(pkg) } @@ -128,7 +128,7 @@ func (state *golistState) mustGetEnv() map[string]string { // goListDriver uses the go list command to interpret the patterns and produce // the build system package structure. // See driver for more details. -func goListDriver(cfg *Config, patterns ...string) (*driverResponse, error) { +func goListDriver(cfg *Config, patterns ...string) (_ *DriverResponse, err error) { // Make sure that any asynchronous go commands are killed when we return. parentCtx := cfg.Context if parentCtx == nil { @@ -146,16 +146,18 @@ func goListDriver(cfg *Config, patterns ...string) (*driverResponse, error) { } // Fill in response.Sizes asynchronously if necessary. - var sizeserr error - var sizeswg sync.WaitGroup if cfg.Mode&NeedTypesSizes != 0 || cfg.Mode&NeedTypes != 0 { - sizeswg.Add(1) + errCh := make(chan error) go func() { compiler, arch, err := packagesdriver.GetSizesForArgsGolist(ctx, state.cfgInvocation(), cfg.gocmdRunner) - sizeserr = err response.dr.Compiler = compiler response.dr.Arch = arch - sizeswg.Done() + errCh <- err + }() + defer func() { + if sizesErr := <-errCh; sizesErr != nil { + err = sizesErr + } }() } @@ -208,10 +210,7 @@ extractQueries: } } - sizeswg.Wait() - if sizeserr != nil { - return nil, sizeserr - } + // (We may yet return an error due to defer.) return response.dr, nil } @@ -266,7 +265,7 @@ func (state *golistState) runContainsQueries(response *responseDeduper, queries // adhocPackage attempts to load or construct an ad-hoc package for a given // query, if the original call to the driver produced inadequate results. -func (state *golistState) adhocPackage(pattern, query string) (*driverResponse, error) { +func (state *golistState) adhocPackage(pattern, query string) (*DriverResponse, error) { response, err := state.createDriverResponse(query) if err != nil { return nil, err @@ -357,7 +356,7 @@ func otherFiles(p *jsonPackage) [][]string { // createDriverResponse uses the "go list" command to expand the pattern // words and return a response for the specified packages. -func (state *golistState) createDriverResponse(words ...string) (*driverResponse, error) { +func (state *golistState) createDriverResponse(words ...string) (*DriverResponse, error) { // go list uses the following identifiers in ImportPath and Imports: // // "p" -- importable package or main (command) @@ -384,7 +383,7 @@ func (state *golistState) createDriverResponse(words ...string) (*driverResponse pkgs := make(map[string]*Package) additionalErrors := make(map[string][]Error) // Decode the JSON and convert it to Package form. - response := &driverResponse{ + response := &DriverResponse{ GoVersion: goVersion, } for dec := json.NewDecoder(buf); dec.More(); { diff --git a/go/packages/packages.go b/go/packages/packages.go index 81e9e6a727d..f33b0afc22c 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -206,43 +206,6 @@ type Config struct { Overlay map[string][]byte } -// driver is the type for functions that query the build system for the -// packages named by the patterns. -type driver func(cfg *Config, patterns ...string) (*driverResponse, error) - -// driverResponse contains the results for a driver query. -type driverResponse struct { - // NotHandled is returned if the request can't be handled by the current - // driver. If an external driver returns a response with NotHandled, the - // rest of the driverResponse is ignored, and go/packages will fallback - // to the next driver. If go/packages is extended in the future to support - // lists of multiple drivers, go/packages will fall back to the next driver. - NotHandled bool - - // Compiler and Arch are the arguments pass of types.SizesFor - // to get a types.Sizes to use when type checking. - Compiler string - Arch string - - // Roots is the set of package IDs that make up the root packages. - // We have to encode this separately because when we encode a single package - // we cannot know if it is one of the roots as that requires knowledge of the - // graph it is part of. - Roots []string `json:",omitempty"` - - // Packages is the full set of packages in the graph. - // The packages are not connected into a graph. - // The Imports if populated will be stubs that only have their ID set. - // Imports will be connected and then type and syntax information added in a - // later pass (see refine). - Packages []*Package - - // GoVersion is the minor version number used by the driver - // (e.g. the go command on the PATH) when selecting .go files. - // Zero means unknown. - GoVersion int -} - // Load loads and returns the Go packages named by the given patterns. // // Config specifies loading options; @@ -291,7 +254,7 @@ func Load(cfg *Config, patterns ...string) ([]*Package, error) { // no external driver, or the driver returns a response with NotHandled set, // defaultDriver will fall back to the go list driver. // The boolean result indicates that an external driver handled the request. -func defaultDriver(cfg *Config, patterns ...string) (*driverResponse, bool, error) { +func defaultDriver(cfg *Config, patterns ...string) (*DriverResponse, bool, error) { if driver := findExternalDriver(cfg); driver != nil { response, err := driver(cfg, patterns...) if err != nil { @@ -303,7 +266,10 @@ func defaultDriver(cfg *Config, patterns ...string) (*driverResponse, bool, erro } response, err := goListDriver(cfg, patterns...) - return response, false, err + if err != nil { + return nil, false, err + } + return response, false, nil } // A Package describes a loaded Go package. @@ -648,7 +614,7 @@ func newLoader(cfg *Config) *loader { // refine connects the supplied packages into a graph and then adds type // and syntax information as requested by the LoadMode. -func (ld *loader) refine(response *driverResponse) ([]*Package, error) { +func (ld *loader) refine(response *DriverResponse) ([]*Package, error) { roots := response.Roots rootMap := make(map[string]int, len(roots)) for i, root := range roots { diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 6e461c8acad..e5687babfa7 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -2953,3 +2953,43 @@ func TestExportFile(t *testing.T) { cfg.Mode = packages.NeedTypes packages.Load(cfg, "fmt") } + +// TestLoadEitherSucceedsOrFails is an attempt to reproduce a sporadic +// failure observed on the Android emu builders in which Load would +// return an empty list of packages but no error. We don't expect +// packages.Load to succeed on that platform, and testenv.NeedsGoBuild +// would ordinarily suppress the attempt if called early. But +// regardless of whether the 'go' command is functional, Load should +// never return an empty set of packages but no error. +func TestLoadEitherSucceedsOrFails(t *testing.T) { + const src = `package p` + dir := t.TempDir() + cfg := &packages.Config{ + Dir: dir, + Mode: packages.LoadSyntax, + Overlay: map[string][]byte{ + filepath.Join(dir, "p.go"): []byte(src), + }, + } + initial, err := packages.Load(cfg, "./p.go") + if err != nil { + // If Load failed because it needed 'go' and the + // platform doesn't have it, silently skip the test. + testenv.NeedsGoBuild(t) + + // Otherwise, it's a real failure. + t.Fatal(err) + } + + // If Load returned without error, + // it had better give us error-free packages. + if packages.PrintErrors(initial) > 0 { + t.Errorf("packages contain errors") + } + + // If Load returned without error, + // it had better give us the correct number packages. + if len(initial) != 1 { + t.Errorf("Load returned %d packages (want 1) and no error", len(initial)) + } +} diff --git a/go/ssa/dom.go b/go/ssa/dom.go index 66a2f5e6ed3..02c1ae83ae3 100644 --- a/go/ssa/dom.go +++ b/go/ssa/dom.go @@ -40,20 +40,25 @@ func (b *BasicBlock) Dominates(c *BasicBlock) bool { return b.dom.pre <= c.dom.pre && c.dom.post <= b.dom.post } -type byDomPreorder []*BasicBlock - -func (a byDomPreorder) Len() int { return len(a) } -func (a byDomPreorder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a byDomPreorder) Less(i, j int) bool { return a[i].dom.pre < a[j].dom.pre } - -// DomPreorder returns a new slice containing the blocks of f in -// dominator tree preorder. +// DomPreorder returns a new slice containing the blocks of f +// in a preorder traversal of the dominator tree. func (f *Function) DomPreorder() []*BasicBlock { - n := len(f.Blocks) - order := make(byDomPreorder, n) - copy(order, f.Blocks) - sort.Sort(order) - return order + slice := append([]*BasicBlock(nil), f.Blocks...) + sort.Slice(slice, func(i, j int) bool { + return slice[i].dom.pre < slice[j].dom.pre + }) + return slice +} + +// DomPostorder returns a new slice containing the blocks of f +// in a postorder traversal of the dominator tree. +// (This is not the same as a postdominance order.) +func (f *Function) DomPostorder() []*BasicBlock { + slice := append([]*BasicBlock(nil), f.Blocks...) + sort.Slice(slice, func(i, j int) bool { + return slice[i].dom.post < slice[j].dom.post + }) + return slice } // domInfo contains a BasicBlock's dominance information. diff --git a/go/ssa/dom_test.go b/go/ssa/dom_test.go new file mode 100644 index 00000000000..f78c7a6909a --- /dev/null +++ b/go/ssa/dom_test.go @@ -0,0 +1,59 @@ +// Copyright 2024 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 ssa_test + +import ( + "fmt" + "path/filepath" + "testing" + + "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testenv" +) + +func TestDominatorOrder(t *testing.T) { + testenv.NeedsGoBuild(t) // for go/packages + + const src = `package p + +func f(cond bool) { + // (Print operands match BasicBlock IDs.) + print(0) + if cond { + print(1) + } else { + print(2) + } + print(3) +} +` + dir := t.TempDir() + cfg := &packages.Config{ + Dir: dir, + Mode: packages.LoadSyntax, + Overlay: map[string][]byte{ + filepath.Join(dir, "p.go"): []byte(src), + }, + } + initial, err := packages.Load(cfg, "./p.go") + if err != nil { + t.Fatal(err) + } + if packages.PrintErrors(initial) > 0 { + t.Fatal("packages contain errors") + } + _, pkgs := ssautil.Packages(initial, 0) + p := pkgs[0] + p.Build() + f := p.Func("f") + + if got, want := fmt.Sprint(f.DomPreorder()), "[0 1 2 3]"; got != want { + t.Errorf("DomPreorder: got %v, want %s", got, want) + } + if got, want := fmt.Sprint(f.DomPostorder()), "[1 2 3 0]"; got != want { + t.Errorf("DomPostorder: got %v, want %s", got, want) + } +} diff --git a/go/types/typeutil/map.go b/go/types/typeutil/map.go index 544246dac1c..e154be0bd60 100644 --- a/go/types/typeutil/map.go +++ b/go/types/typeutil/map.go @@ -12,6 +12,7 @@ import ( "go/types" "reflect" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -259,6 +260,9 @@ func (h Hasher) hashFor(t types.Type) uint32 { case *types.Basic: return uint32(t.Kind()) + case *aliases.Alias: + return h.Hash(t.Underlying()) + case *types.Array: return 9043 + 2*uint32(t.Len()) + 3*h.Hash(t.Elem()) @@ -457,6 +461,9 @@ func (h Hasher) shallowHash(t types.Type) uint32 { // elements (mostly Slice, Pointer, Basic, Named), // so there's no need to optimize anything else. switch t := t.(type) { + case *aliases.Alias: + return h.shallowHash(t.Underlying()) + case *types.Signature: var hash uint32 = 604171 if t.Variadic() { diff --git a/go/types/typeutil/map_test.go b/go/types/typeutil/map_test.go index 4891f7687d5..2cc1de786dc 100644 --- a/go/types/typeutil/map_test.go +++ b/go/types/typeutil/map_test.go @@ -247,6 +247,15 @@ var Issue56048 = Issue56048_I.m type Issue56048_Ib interface{ m() chan []*interface { Issue56048_Ib } } var Issue56048b = Issue56048_Ib.m +// Non-generic alias +type NonAlias int +type Alias1 = NonAlias +type Alias2 = NonAlias + +// Generic alias (requires go1.23) +// type SetOfInt = map[int]bool +// type Set[T comparable] = map[K]bool +// type SetOfInt2 = Set[int] ` fset := token.NewFileSet() @@ -307,6 +316,16 @@ var Issue56048b = Issue56048_Ib.m Quux = scope.Lookup("Quux").Type() Issue56048 = scope.Lookup("Issue56048").Type() Issue56048b = scope.Lookup("Issue56048b").Type() + + // In go1.23 these will be *types.Alias; for now they are all int. + NonAlias = scope.Lookup("NonAlias").Type() + Alias1 = scope.Lookup("Alias1").Type() + Alias2 = scope.Lookup("Alias2").Type() + + // Requires go1.23. + // SetOfInt = scope.Lookup("SetOfInt").Type() + // Set = scope.Lookup("Set").Type().(*types.Alias) + // SetOfInt2 = scope.Lookup("SetOfInt2").Type() ) tmap := new(typeutil.Map) @@ -379,6 +398,16 @@ var Issue56048b = Issue56048_Ib.m {Issue56048, "Issue56048", true}, // (not actually about generics) {Issue56048b, "Issue56048b", true}, // (not actually about generics) + + // All three types are identical. + {NonAlias, "NonAlias", true}, + {Alias1, "Alias1", false}, + {Alias2, "Alias2", false}, + + // Generic aliases: requires go1.23. + // {SetOfInt, "SetOfInt", true}, + // {Set, "Set", false}, + // {SetOfInt2, "SetOfInt2", false}, } for _, step := range steps { diff --git a/go/types/typeutil/methodsetcache.go b/go/types/typeutil/methodsetcache.go index a5d9310830c..bd71aafaaa1 100644 --- a/go/types/typeutil/methodsetcache.go +++ b/go/types/typeutil/methodsetcache.go @@ -9,6 +9,8 @@ package typeutil import ( "go/types" "sync" + + "golang.org/x/tools/internal/aliases" ) // A MethodSetCache records the method set of each type T for which @@ -32,12 +34,12 @@ func (cache *MethodSetCache) MethodSet(T types.Type) *types.MethodSet { cache.mu.Lock() defer cache.mu.Unlock() - switch T := T.(type) { + switch T := aliases.Unalias(T).(type) { case *types.Named: return cache.lookupNamed(T).value case *types.Pointer: - if N, ok := T.Elem().(*types.Named); ok { + if N, ok := aliases.Unalias(T.Elem()).(*types.Named); ok { return cache.lookupNamed(N).pointer } } diff --git a/gopls/README.md b/gopls/README.md index 396f86c0242..5c80965c153 100644 --- a/gopls/README.md +++ b/gopls/README.md @@ -94,6 +94,7 @@ version of gopls. | Go 1.12 | [gopls@v0.7.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.7.5) | | Go 1.15 | [gopls@v0.9.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.9.5) | | Go 1.17 | [gopls@v0.11.0](https://github.com/golang/tools/releases/tag/gopls%2Fv0.11.0) | +| Go 1.18 | [gopls@v0.14.2](https://github.com/golang/tools/releases/tag/gopls%2Fv0.14.2) | Our extended support is enforced via [continuous integration with older Go versions](doc/contributing.md#ci). This legacy Go CI may not block releases: diff --git a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go index ffe9254ab95..dfa1cb69e55 100644 --- a/gopls/api-diff/api_diff.go +++ b/gopls/api-diff/api_diff.go @@ -9,7 +9,6 @@ package main import ( "bytes" - "context" "encoding/json" "flag" "fmt" @@ -50,8 +49,7 @@ func main() { } func diffAPI(oldVer, newVer string) (string, error) { - ctx := context.Background() - previousAPI, err := loadAPI(ctx, oldVer) + previousAPI, err := loadAPI(oldVer) if err != nil { return "", fmt.Errorf("loading %s: %v", oldVer, err) } @@ -60,7 +58,7 @@ func diffAPI(oldVer, newVer string) (string, error) { currentAPI = settings.GeneratedAPIJSON } else { var err error - currentAPI, err = loadAPI(ctx, newVer) + currentAPI, err = loadAPI(newVer) if err != nil { return "", fmt.Errorf("loading %s: %v", newVer, err) } @@ -69,7 +67,7 @@ func diffAPI(oldVer, newVer string) (string, error) { return cmp.Diff(previousAPI, currentAPI), nil } -func loadAPI(ctx context.Context, version string) (*settings.APIJSON, error) { +func loadAPI(version string) (*settings.APIJSON, error) { ver := fmt.Sprintf("golang.org/x/tools/gopls@%s", version) cmd := exec.Command("go", "run", ver, "api-json") diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 51eec8a90dc..0a469a14ccd 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -263,6 +263,29 @@ known as "false sharing" that slows down both goroutines. **Disabled by default. Enable it by setting `"analyses": {"fieldalignment": true}`.** +## **fillreturns** + +fillreturns: suggest fixes for errors due to an incorrect number of return values + +This checker provides suggested fixes for type errors of the +type "wrong number of return values (want %d, got %d)". For example: + + func m() (int, string, *bool, error) { + return + } + +will turn into + + func m() (int, string, *bool, error) { + return 0, "", nil, nil + } + +This functionality is similar to https://github.com/sqs/goreturns. + +[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns) + +**Enabled by default.** + ## **httpresponse** httpresponse: check for mistakes using HTTP responses @@ -306,6 +329,24 @@ io.Reader, so this assertion cannot succeed. **Enabled by default.** +## **infertypeargs** + +infertypeargs: check for unnecessary type arguments in call expressions + +Explicit type arguments may be omitted from call expressions if they can be +inferred from function arguments, or from other type arguments: + + func f[T any](T) {} + + func _() { + f[string]("foo") // string could be inferred + } + + +[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/infertypeargs) + +**Enabled by default.** + ## **loopclosure** loopclosure: check references to loop variables from within nested functions @@ -442,6 +483,43 @@ and: **Enabled by default.** +## **nonewvars** + +nonewvars: suggested fixes for "no new vars on left side of :=" + +This checker provides suggested fixes for type errors of the +type "no new vars on left side of :=". For example: + + z := 1 + z := 2 + +will turn into + + z := 1 + z = 2 + +[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/nonewvars) + +**Enabled by default.** + +## **noresultvalues** + +noresultvalues: suggested fixes for unexpected return values + +This checker provides suggested fixes for type errors of the +type "no result values expected" or "too many return values". +For example: + + func z() { return nil } + +will turn into + + func z() { return } + +[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/noresultvars) + +**Enabled by default.** + ## **printf** printf: check consistency of Printf format strings and arguments @@ -673,6 +751,42 @@ Also report certain struct tags (json, xml) used with unexported fields. **Enabled by default.** +## **stubmethods** + +stubmethods: detect missing methods and fix with stub implementations + +This analyzer detects type-checking errors due to missing methods +in assignments from concrete types to interface types, and offers +a suggested fix that will create a set of stub methods so that +the concrete type satisfies the interface. + +For example, this function will not compile because the value +NegativeErr{} does not implement the "error" interface: + + func sqrt(x float64) (float64, error) { + if x < 0 { + return 0, NegativeErr{} // error: missing method + } + ... + } + + type NegativeErr struct{} + +This analyzer will suggest a fix to declare this method: + + // Error implements error.Error. + func (NegativeErr) Error() string { + panic("unimplemented") + } + +(At least, it appears to behave that way, but technically it +doesn't use the SuggestedFix mechanism and the stub is created by +logic in gopls's golang.stub function.) + +[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods) + +**Enabled by default.** + ## **testinggoroutine** testinggoroutine: report calls to (*testing.T).Fatal from goroutines started by a test @@ -719,6 +833,26 @@ standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended. **Enabled by default.** +## **undeclaredname** + +undeclaredname: suggested fixes for "undeclared name: <>" + +This checker provides suggested fixes for type errors of the +type "undeclared name: <>". It will either insert a new statement, +such as: + + <> := + +or a new function declaration, such as: + + func <>(inferred parameters) { + panic("implement me!") + } + +[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/undeclaredname) + +**Enabled by default.** + ## **unmarshal** unmarshal: report passing non-pointer or non-interface values to unmarshal @@ -763,15 +897,29 @@ unusedparams: check for unused parameters of functions The unusedparams analyzer checks functions to see if there are any parameters that are not being used. -To reduce false positives it ignores: -- methods -- parameters that do not have a name or have the name '_' (the blank identifier) -- functions in test files -- functions with empty bodies or those with just a return stmt +To ensure soundness, it ignores: + - "address-taken" functions, that is, functions that are used as + a value rather than being called directly; their signatures may + be required to conform to a func type. + - exported functions or methods, since they may be address-taken + in another package. + - unexported methods whose name matches an interface method + declared in the same package, since the method's signature + may be required to conform to the interface type. + - functions with empty bodies, or containing just a call to panic. + - parameters that are unnamed, or named "_", the blank identifier. + +The analyzer suggests a fix of replacing the parameter name by "_", +but in such cases a deeper fix can be obtained by invoking the +"Refactor: remove unused parameter" code action, which will +eliminate the parameter entirely, along with all corresponding +arguments at call sites, while taking care to preserve any side +effects in the argument expressions; see +https://github.com/golang/tools/releases/tag/gopls%2Fv0.14. [Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams) -**Disabled by default. Enable it by setting `"analyses": {"unusedparams": true}`.** +**Enabled by default.** ## **unusedresult** @@ -789,6 +937,14 @@ The set of functions may be controlled using flags. **Enabled by default.** +## **unusedvariable** + +unusedvariable: check for unused variables and suggest fixes + +[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable) + +**Disabled by default. Enable it by setting `"analyses": {"unusedvariable": true}`.** + ## **unusedwrite** unusedwrite: checks for unused writes @@ -829,160 +985,4 @@ useany: check for constraints that could be simplified to "any" **Disabled by default. Enable it by setting `"analyses": {"useany": true}`.** -## **fillreturns** - -fillreturns: suggest fixes for errors due to an incorrect number of return values - -This checker provides suggested fixes for type errors of the -type "wrong number of return values (want %d, got %d)". For example: - - func m() (int, string, *bool, error) { - return - } - -will turn into - - func m() (int, string, *bool, error) { - return 0, "", nil, nil - } - -This functionality is similar to https://github.com/sqs/goreturns. - -[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns) - -**Enabled by default.** - -## **nonewvars** - -nonewvars: suggested fixes for "no new vars on left side of :=" - -This checker provides suggested fixes for type errors of the -type "no new vars on left side of :=". For example: - - z := 1 - z := 2 - -will turn into - - z := 1 - z = 2 - -[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/nonewvars) - -**Enabled by default.** - -## **noresultvalues** - -noresultvalues: suggested fixes for unexpected return values - -This checker provides suggested fixes for type errors of the -type "no result values expected" or "too many return values". -For example: - - func z() { return nil } - -will turn into - - func z() { return } - -[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/noresultvars) - -**Enabled by default.** - -## **undeclaredname** - -undeclaredname: suggested fixes for "undeclared name: <>" - -This checker provides suggested fixes for type errors of the -type "undeclared name: <>". It will either insert a new statement, -such as: - - <> := - -or a new function declaration, such as: - - func <>(inferred parameters) { - panic("implement me!") - } - -[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/undeclaredname) - -**Enabled by default.** - -## **unusedvariable** - -unusedvariable: check for unused variables and suggest fixes - -[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable) - -**Disabled by default. Enable it by setting `"analyses": {"unusedvariable": true}`.** - -## **fillstruct** - -fillstruct: note incomplete struct initializations - -This analyzer provides diagnostics for any struct literals that do not have -any fields initialized. Because the suggested fix for this analysis is -expensive to compute, callers should compute it separately, using the -SuggestedFix function below. - - -[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillstruct) - -**Enabled by default.** - -## **infertypeargs** - -infertypeargs: check for unnecessary type arguments in call expressions - -Explicit type arguments may be omitted from call expressions if they can be -inferred from function arguments, or from other type arguments: - - func f[T any](T) {} - - func _() { - f[string]("foo") // string could be inferred - } - - -[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/infertypeargs) - -**Enabled by default.** - -## **stubmethods** - -stubmethods: detect missing methods and fix with stub implementations - -This analyzer detects type-checking errors due to missing methods -in assignments from concrete types to interface types, and offers -a suggested fix that will create a set of stub methods so that -the concrete type satisfies the interface. - -For example, this function will not compile because the value -NegativeErr{} does not implement the "error" interface: - - func sqrt(x float64) (float64, error) { - if x < 0 { - return 0, NegativeErr{} // error: missing method - } - ... - } - - type NegativeErr struct{} - -This analyzer will suggest a fix to declare this method: - - // Error implements error.Error. - func (NegativeErr) Error() string { - panic("unimplemented") - } - -(At least, it appears to behave that way, but technically it -doesn't use the SuggestedFix mechanism and the stub is created by -logic in gopls's source.stub function.) - -[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods) - -**Enabled by default.** - diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index a838c73df6b..6a651faf467 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -66,7 +66,14 @@ Args: ``` { - // The fix to apply. + // The name of the fix to apply. + // + // For fixes suggested by analyzers, this is a string constant + // advertised by the analyzer that matches the Category of + // the analysis.Diagnostic with a SuggestedFix containing no edits. + // + // For fixes suggested by code actions, this is a string agreed + // upon by the code action and golang.ApplyFix. "Fix": string, // The file URI for the document to fix. "URI": string, @@ -81,6 +88,47 @@ Args: "character": uint32, }, }, + // Whether to resolve and return the edits. + "ResolveEdits": bool, +} +``` + +Result: + +``` +{ + // Holds changes to existing resources. + "changes": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit, + // Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes + // are either an array of `TextDocumentEdit`s to express changes to n different text documents + // where each text document edit addresses a specific version of a text document. Or it can contain + // above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. + // + // Whether a client supports versioned document edits is expressed via + // `workspace.workspaceEdit.documentChanges` client capability. + // + // If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then + // only plain `TextEdit`s using the `changes` property are supported. + "documentChanges": []{ + "TextDocumentEdit": { + "textDocument": { ... }, + "edits": { ... }, + }, + "RenameFile": { + "kind": string, + "oldUri": string, + "newUri": string, + "options": { ... }, + "ResourceOperation": { ... }, + }, + }, + // A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and + // delete file / folder operations. + // + // Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. + // + // @since 3.16.0 + "changeAnnotations": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation, } ``` @@ -101,6 +149,47 @@ Args: "end": { ... }, }, }, + // Whether to resolve and return the edits. + "ResolveEdits": bool, +} +``` + +Result: + +``` +{ + // Holds changes to existing resources. + "changes": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit, + // Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes + // are either an array of `TextDocumentEdit`s to express changes to n different text documents + // where each text document edit addresses a specific version of a text document. Or it can contain + // above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. + // + // Whether a client supports versioned document edits is expressed via + // `workspace.workspaceEdit.documentChanges` client capability. + // + // If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then + // only plain `TextEdit`s using the `changes` property are supported. + "documentChanges": []{ + "TextDocumentEdit": { + "textDocument": { ... }, + "edits": { ... }, + }, + "RenameFile": { + "kind": string, + "oldUri": string, + "newUri": string, + "options": { ... }, + "ResourceOperation": { ... }, + }, + }, + // A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and + // delete file / folder operations. + // + // Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. + // + // @since 3.16.0 + "changeAnnotations": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation, } ``` @@ -166,7 +255,7 @@ Args: Result: ``` -map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result +map[golang.org/x/tools/gopls/internal/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result ``` ### **Toggle gc_details** diff --git a/gopls/doc/contributing.md b/gopls/doc/contributing.md index aee90294e0c..a2f987b63c9 100644 --- a/gopls/doc/contributing.md +++ b/gopls/doc/contributing.md @@ -18,8 +18,8 @@ claiming it. ## Getting started -Most of the `gopls` logic is in the `golang.org/x/tools/gopls/internal/lsp` -directory. +Most of the `gopls` logic is in the `golang.org/x/tools/gopls/internal` +directory. See [design/implementation.md] for an overview of the code organization. ## Build @@ -94,41 +94,43 @@ Users are invited to share it if they are willing. ## Testing -To run tests for just `gopls/`, run, +The normal command you should use to run the tests after a change is: ```bash -cd /path/to/tools/gopls -go test ./... -``` - -But, much of the gopls work involves `internal/lsp` too, so you will want to -run both: - -```bash -cd /path/to/tools -cd gopls && go test ./... -cd .. -go test ./internal/lsp/... +gopls$ go test -short ./... ``` -There is additional information about the `internal/lsp` tests in the -[internal/lsp/tests `README`](https://github.com/golang/tools/blob/master/internal/lsp/tests/README.md). - -### Integration tests - -gopls has a suite of integration tests defined in the `./gopls/internal/test/integration` -directory. Each of these tests writes files to a temporary directory, starts a -separate gopls session, and scripts interactions using an editor-like API. As a -result of this overhead they can be quite slow, particularly on systems where -file operations are costly. - -Due to the asynchronous nature of the LSP, integration tests assert -'expectations' that the editor state must achieve _eventually_. This can -make debugging the integration tests difficult. To aid with debugging, the tests -output their LSP logs on any failure. If your CL gets a test failure while -running the tests, please do take a look at the description of the error and -the LSP logs, but don't hesitate to [reach out](#getting-help) to the gopls -team if you need help. +(The `-short` flag skips some slow-running ones. The trybot builders +run the complete set, on a wide range of platforms.) + +Gopls tests are a mix of two kinds. + +- [Marker tests](../internal/test/marker) express each test scenario + in a standalone text file that contains the target .go, go.mod, and + go.work files, in which special annotations embedded in comments + drive the test. These tests are generally easy to write and fast + to iterate, but have limitations on what they can express. + +- [Integration tests](../internal/test/integration) are regular Go + `func Test(*testing.T)` functions that make a series of calls to an + API for a fake LSP-enabled client editor. The API allows you to open + and edit a file, navigate to a definition, invoke other LSP + operations, and assert properties about the state. + + Due to the asynchronous nature of the LSP, integration tests make + assertions about states that the editor must achieve eventually, + even when the program goes wrong quickly, it may take a while before + the error is reported as a failure to achieve the desired state + within several minutes. We recommend that you set + `GOPLS_INTEGRATION_TEST_TIMEOUT=10s` to reduce the timeout for + integration tests when debugging. + + When they fail, the integration tests print the log of the LSP + session between client and server. Though verbose, they are very + helpful for debugging once you know how to read them. + +Don't hesitate to [reach out](#getting-help) to the gopls team if you +need help. ### CI diff --git a/gopls/doc/design/architecture.svg b/gopls/doc/design/architecture.svg new file mode 100644 index 00000000000..6c554d5670c --- /dev/null +++ b/gopls/doc/design/architecture.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gopls/doc/design/implementation.md b/gopls/doc/design/implementation.md index f8d734f92ef..12d655c0b5e 100644 --- a/gopls/doc/design/implementation.md +++ b/gopls/doc/design/implementation.md @@ -1,46 +1,172 @@ -# gopls implementation documentation -This is not intended as a complete description of the implementation, for the most the part the package godoc, code comments and the code itself hold that. -Instead this is meant to be a guide into finding parts of the implementation, and understanding some core concepts used throughout the implementation. +# Gopls architecture -## View/Session/Cache +Last major update: Jan 16 2024 -Throughout the code there are references to these three concepts, and they build on each other. +This doc presents a high-level overview of the structure of gopls to +help new contributors find their way. It is not intended to be a +complete description of the implementation, nor even of any key +components; for that, the package documentation (linked below) and +other comments within the code are a better guide. -At the base is the *Cache*. This is the level at which we hold information that is global in nature, for instance information about the file system and its contents. +The diagram below shows selected components of the gopls module and +their relationship to each other according to the Go import graph. +Tests and test infrastructure are not shown, nor are utility packages, +nor packages from the [x/tools] module. For brevity, packages are +referred to by their last segment, which is usually unambiguous. -Above that is the *Session*, which holds information for a connection to an editor. This layer hold things like the edited files (referred to as overlays). +The height of each blob corresponds loosely to its technical depth. +Some blocks are wide and shallow, such as [protocol], which declares +Go types for the entire LSP protocol. Others are deep, such as [cache] +and [golang], as they contain a lot of dense logic and algorithms. -The top layer is called the *View*. This holds the configuration, and the mapping to configured packages. + +![Gopls architecture](architecture.svg) -The purpose of this layering is to allow a single editor session to have multiple views active whilst still sharing as much information as possible for efficiency. -In theory if only the View layer existed, the results would be identical, but slower and using more memory. +Starting from the bottom, we'll describe the various components. -## Code location +The lowest layer defines the request and response types of the +Language Server Protocol: -gopls will be developed in the [x/tools] Go repository; the core packages are in [internal/lsp], and the binary and integration tests are located in [gopls]. +- The [protocol] package defines the standard protocol; it is mostly + generated mechanically from the schema definition provided by + Microsoft. + The most important type is DocumentURI, which represents a `file:` + URL that identifies a client editor document. It also provides + `Mapper`, which maps between the different coordinate systems used + for source positions: UTF-8, UTF-16, and token.Pos. -Below is a list of the core packages of gopls, and their primary purpose: +- The [command] package defines Gopls's non-standard commands, which + are all invoked through the `workspace/executeCommand` extension + mechanism. These commands are typically returned by the server as + continuations of Code Actions or Code Lenses; most clients do not + construct calls to them directly. -Package | Description ---- | --- -[gopls] | the main binary, plugins and integration tests -[internal/lsp] | the core message handling package -[internal/lsp/cache] | the cache layer -[internal/cmd] | the gopls command line layer -[internal/debug] | features to aid in debugging gopls -[internal/lsp/protocol] | the types of LSP request and response messages -[internal/lsp/source] | the core feature implementations -[internal/memoize] | a function invocation cache used to reduce the work done -[internal/jsonrpc2] | an implementation of the JSON RPC2 specification +The next layer defines a number of important and very widely used data structures: -[gopls]: https://github.com/golang/tools/tree/master/gopls -[internal/jsonrpc2]: https://github.com/golang/tools/tree/master/internal/jsonrpc2 -[internal/lsp]: https://github.com/golang/tools/tree/master/gopls/internal/lsp -[internal/lsp/cache]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/cache -[internal/cmd]: https://github.com/golang/tools/tree/master/gopls/internal/cmd -[internal/debug]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/debug -[internal/lsp/source]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/source -[internal/memoize]: https://github.com/golang/tools/tree/master/internal/memoize -[internal/lsp/protocol]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/protocol -[x/tools]: https://github.com/golang/tools +- The [file] package defines the primary abstractions of a client + file: its `Identity` (URI and content hash), and its `Handle` (which + additionally provides the version and content of a particular + snapshot of the file. + +- The [parsego] package defines `File`, the parsed form of a Go source + file, including its content, syntax tree, and coordinary mappings + (Mapper and token.File). The package performs various kinds of tree + repair to work around error-recovery shortcomings of the Go parser. + +- The [metadata] package defines `Package`, an abstraction of the + metadata of a Go package, similar to the output of `go list -json`. + Metadata is produced from [go/packages], which takes + care of invoking `go list`. (Users report that it works to some extent + with a GOPACKAGESDRIVER for Bazel, though we maintain no tests for this + scenario.) + + The package also provides `Graph`, the complete import graph for a + workspace; each graph node is a `Package`. + +The [settings] layer defines the data structure (effectively a large +tree) for gopls configuration options, along with its JSON encoding. + +The [cache] layer is the largest and most complex component of gopls. +It is concerned with state management, dependency analysis, and invalidation: +the `Session` of communication with the client; +the `Folder`s that the client has opened; +the `View` of a particular workspace tree with particular build +options; +the `Snapshot` of the state of all files in the workspace after a +particular edit operation; +the contents of all files, whether saved to disk (`DiskFile`) or +edited and unsaved (`Overlay`); +the `Cache` of in-memory memoized computations, +such as parsing go.mod files or build the symbol index; +and the `Package`, which holds the results of type checking a package +from Go syntax. + +The cache layer depends on various auxiliary packages, including: + +- The [filecache] package, which manages gopls' persistent, transactional, + file-based key/value store. + +- The [xrefs], [methodsets], and [typerefs] packages define algorithms + for constructing indexes of information derived from type-checking, + and for encoding and decoding these serializable indexes in the file + cache. + + Together these packages enable the fast restart, reduced memory + consumption, and synergy across processes that were delivered by the + v0.12 redesign and described in ["Scaling gopls for the growing Go + ecosystem"](https://go.dev/blog/gopls-scalability). + +The cache also defines gopls's [go/analysis] driver, which runs +modular analysis (similar to `go vet`) across the workspace. +Gopls also includes a number of analysis passes that are not part of vet. + +The next layer defines four packages, each for handling files in a +particular language: +[mod] for go.mod files; +[work] for go.work files; +[template] for files in `text/template` syntax; and +[golang], for files in Go itself. +This package, by far the largest, provides the main features of gopls: +navigation, analysis, and refactoring of Go code. +As most users imagine it, this package _is_ gopls. + +The [server] package defines the LSP service implementation, with one +handler method per LSP request type. Each handler switches on the type +of the file and dispatches to one of the four language-specific +packages. + +The [lsprpc] package connects the service interface to our [JSON RPC](jsonrpc2) +server. + +Bear in mind that the diagram is a dependency graph, a "static" +viewpoint of the program's structure. A more dynamic viewpoint would +order the packages based on the sequence in which they are encountered +during processing of a particular request; in such a view, the bottom +layer would represent the "wire" (protocol and command), the next +layer up would hold the RPC-related packages (lsprpc and server), and +features (e.g. golang, mod, work, template) would be at the top. + + + +The [cmd] package defines the command-line interface of the `gopls` +command, around which gopls's main package is just a trivial wrapper. +It is usually run without arguments, causing it to start a server and +listen indefinitely. +It also provides a number of subcommands that start a server, make a +single request to it, and exit, providing traditional batch-command +access to server functionality. These subcommands are primarily +provided as a debugging aid (but see +[#63693](https://github.com/golang/go/issues/63693)). + +[cache]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache +[cmd]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cmd +[command]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/protocol/command +[debug]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/debug +[file]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/file +[filecache]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/filecache +[go/analysis]: https://pkg.go.dev/golang.org/x/tools@master/go/analysis +[go/packages]: https://pkg.go.dev/golang.org/x/tools@master/go/packages +[gopls]: https://pkg.go.dev/golang.org/x/tools/gopls@master +[jsonrpc2]: https://pkg.go.dev/golang.org/x/tools@master/internal/jsonrpc2 +[lsprpc]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/lsprpc +[memoize]: https://github.com/golang/tools/tree/master/internal/memoize +[metadata]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/metadata +[methodsets]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/methodsets +[mod]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/mod +[parsego]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/parsego +[protocol]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/protocol +[server]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/server +[settings]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/settings +[golang]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/golang +[template]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/template +[typerefs]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/typerefs +[work]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/work +[x/tools]: https://github.com/golang/tools@master +[xrefs]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/xrefs diff --git a/gopls/doc/design/integrating.md b/gopls/doc/design/integrating.md index 3448402e49e..2d8e01a76c0 100644 --- a/gopls/doc/design/integrating.md +++ b/gopls/doc/design/integrating.md @@ -59,9 +59,9 @@ For instance, files that are needed to do correct type checking are modified by Monitoring files inside gopls directly has a lot of awkward problems, but the [LSP specification] has methods that allow gopls to request that the client notify it of file system changes, specifically [`workspace/didChangeWatchedFiles`]. This is currently being added to gopls by a community member, and tracked in [#31553] -[InitializeResult]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#InitializeResult -[ServerCapabilities]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#ServerCapabilities -[`golang.org/x/tools/gopls/internal/lsp/protocol`]: https://pkg.go.dev/golang.org/x/tools/internal/lsp/protocol#NewPoint +[InitializeResult]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/protocol#InitializeResult +[ServerCapabilities]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/protocol#ServerCapabilities +[`golang.org/x/tools/gopls/internal/protocol`]: https://pkg.go.dev/golang.org/x/tools/internal/protocol#NewPoint [LSP specification]: https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/ [lsp-response]: https://github.com/Microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md#response-message diff --git a/gopls/doc/emacs.md b/gopls/doc/emacs.md index 486f49325cb..8a54cf19d0a 100644 --- a/gopls/doc/emacs.md +++ b/gopls/doc/emacs.md @@ -144,12 +144,14 @@ code action, which you can invoke as needed by running `M-x eglot-code-actions` (or a key of your choice bound to the `eglot-code-actions` function) and selecting `Organize Imports` at the prompt. -Eglot does not currently support a standalone function to execute a specific -code action (see -[joaotavora/eglot#411](https://github.com/joaotavora/eglot/issues/411)), nor an -option to organize imports as a `before-save-hook` (see -[joaotavora/eglot#574](https://github.com/joaotavora/eglot/issues/574)). In the -meantime, see those issues for discussion and possible workarounds. +To automatically organize imports before saving, add a hook: + +```elisp +(add-hook 'before-save-hook + (lambda () + (call-interactively 'eglot-code-action-organize-imports)) + nil t) +``` ## Troubleshooting diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go index ee5c52b0750..595c19a2bdf 100644 --- a/gopls/doc/generate.go +++ b/gopls/doc/generate.go @@ -32,10 +32,10 @@ import ( "github.com/jba/printsrc" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/command/commandmeta" - "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/mod" + "golang.org/x/tools/gopls/internal/protocol/command" + "golang.org/x/tools/gopls/internal/protocol/command/commandmeta" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/safetoken" ) @@ -108,12 +108,13 @@ func loadAPI() (*settings.APIJSON, error) { } pkg := pkgs[0] + defaults := settings.DefaultOptions() api := &settings.APIJSON{ - Options: map[string][]*settings.OptionJSON{}, + Options: map[string][]*settings.OptionJSON{}, + Analyzers: loadAnalyzers(defaults.DefaultAnalyzers), // no staticcheck analyzers } - defaults := settings.DefaultOptions() - api.Commands, err = loadCommands(pkg) + api.Commands, err = loadCommands() if err != nil { return nil, err } @@ -123,15 +124,7 @@ func loadAPI() (*settings.APIJSON, error) { for _, c := range api.Commands { c.Command = command.ID(c.Command) } - for _, m := range []map[string]*settings.Analyzer{ - defaults.DefaultAnalyzers, - defaults.TypeErrorAnalyzers, - defaults.ConvenienceAnalyzers, - // Don't yet add staticcheck analyzers. - } { - api.Analyzers = append(api.Analyzers, loadAnalyzers(m)...) - } - api.Hints = loadHints(source.AllInlayHints) + api.Hints = loadHints(golang.AllInlayHints) for _, category := range []reflect.Value{ reflect.ValueOf(defaults.UserOptions), } { @@ -249,7 +242,7 @@ func loadOptions(category reflect.Value, optsType types.Object, pkg *packages.Pa name := lowerFirst(typesField.Name()) var enumKeys settings.EnumKeys - if m, ok := typesField.Type().(*types.Map); ok { + if m, ok := typesField.Type().Underlying().(*types.Map); ok { e, ok := enums[m.Key()] if ok { typ = strings.Replace(typ, m.Key().String(), m.Key().Underlying().String(), 1) @@ -320,7 +313,7 @@ func collectEnumKeys(name string, m *types.Map, reflectField reflect.Value, enum } // We can get default values for enum -> bool maps. var isEnumBoolMap bool - if basic, ok := m.Elem().(*types.Basic); ok && basic.Kind() == types.Bool { + if basic, ok := m.Elem().Underlying().(*types.Basic); ok && basic.Kind() == types.Bool { isEnumBoolMap = true } for _, v := range enumValues { @@ -410,7 +403,7 @@ func valueDoc(name, value, doc string) string { return fmt.Sprintf("`%s`: %s", value, doc) } -func loadCommands(pkg *packages.Package) ([]*settings.CommandJSON, error) { +func loadCommands() ([]*settings.CommandJSON, error) { var commands []*settings.CommandJSON _, cmds, err := commandmeta.Load() @@ -488,7 +481,7 @@ func structDoc(fields []*commandmeta.Field, level int) string { func loadLenses(commands []*settings.CommandJSON) []*settings.LensJSON { all := map[command.Command]struct{}{} - for k := range source.LensFuncs() { + for k := range golang.LensFuncs() { all[k] = struct{}{} } for k := range mod.LensFuncs() { @@ -531,7 +524,7 @@ func loadAnalyzers(m map[string]*settings.Analyzer) []*settings.AnalyzerJSON { return json } -func loadHints(m map[string]*source.Hint) []*settings.HintJSON { +func loadHints(m map[string]*golang.Hint) []*settings.HintJSON { var sorted []string for _, h := range m { sorted = append(sorted, h.Name) diff --git a/gopls/doc/helix.md b/gopls/doc/helix.md new file mode 100644 index 00000000000..83f923de923 --- /dev/null +++ b/gopls/doc/helix.md @@ -0,0 +1,51 @@ +# Helix + +Configuring `gopls` to work with Helix is rather straightforward. Install `gopls`, and then add it to the `PATH` variable. If it is in the `PATH` variable, Helix will be able to detect it automatically. + +The documentation explaining how to install the default language servers for Helix can be found [here](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) + +## Installing `gopls` + +The first step is to install `gopls` on your machine. +You can follow installation instructions [here](https://github.com/golang/tools/tree/master/gopls#installation). + +## Setting your path to include `gopls` + +Set your `PATH` environment variable to point to `gopls`. +If you used `go install` to download `gopls`, it should be in `$GOPATH/bin`. +If you don't have `GOPATH` set, you can use `go env GOPATH` to find it. + +## Additional information + +You can find more information about how to set up the LSP formatter [here](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers#autoformatting). + +It is possible to use `hx --health go` to see that the language server is properly set up. + +### Configuration + +The settings for `gopls` can be configured in the `languages.toml` file. +The official Helix documentation for this can be found [here](https://docs.helix-editor.com/languages.html) + +Configuration pertaining to `gopls` should be in the table `language-server.gopls`. + +#### How to set flags + +To set flags, add them to the `args` array in the `language-server.gopls` section of the `languages.toml` file. + +#### How to set LSP configuration + +Configuration options can be set in the `language-server.gopls.config` section of the `languages.toml` file, or in the `config` key of the `language-server.gopls` section of the `languages.toml` file. + +#### A minimal config example + +In the `~/.config/helix/languages.toml` file, the following snippet would set up `gopls` with a logfile located at `/tmp/gopls.log` and enable staticcheck. + +```toml +[language-server.gopls] +command = "gopls" +args = ["-logfile=/tmp/gopls.log", "serve"] +[language-server.gopls.config] +"ui.diagnostic.staticcheck" = true +``` + + diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index d6ec1df356b..9f692cf6848 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -1,6 +1,6 @@ # Settings - + This document describes the global settings for `gopls` inside the editor. The settings block will be called `"gopls"` and contains a collection of @@ -286,7 +286,7 @@ Example Usage: ... "analyses": { "unreachable": false, // Disable the unreachable analyzer. - "unusedparams": true // Enable the unusedparams analyzer. + "unusedvariable": true // Enable the unusedvariable analyzer. } ... ``` diff --git a/gopls/go.mod b/gopls/go.mod index 903a7ada8a9..f17fdcaa522 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -3,26 +3,26 @@ module golang.org/x/tools/gopls go 1.18 require ( - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.6.0 github.com/jba/printsrc v0.2.2 github.com/jba/templatecheck v0.6.0 - golang.org/x/mod v0.14.0 + golang.org/x/mod v0.15.0 golang.org/x/sync v0.6.0 - golang.org/x/telemetry v0.0.0-20231114163143-69313e640400 + golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78 golang.org/x/text v0.14.0 - golang.org/x/tools v0.13.1-0.20230920233436-f9b8da7b22be + golang.org/x/tools v0.17.0 golang.org/x/vuln v1.0.1 gopkg.in/yaml.v3 v3.0.1 - honnef.co/go/tools v0.4.5 - mvdan.cc/gofumpt v0.4.0 - mvdan.cc/xurls/v2 v2.4.0 + honnef.co/go/tools v0.4.6 + mvdan.cc/gofumpt v0.6.0 + mvdan.cc/xurls/v2 v2.5.0 ) require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/google/safehtml v0.1.0 // indirect golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/sys v0.17.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/gopls/go.sum b/gopls/go.sum index a4a914744ae..9ab4bd75926 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -1,8 +1,8 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= @@ -10,34 +10,34 @@ github.com/jba/printsrc v0.2.2 h1:9OHK51UT+/iMAEBlQIIXW04qvKyF3/vvLuwW/hL8tDU= github.com/jba/printsrc v0.2.2/go.mod h1:1xULjw59sL0dPdWpDoVU06TIEO/Wnfv6AHRpiElTwYM= github.com/jba/templatecheck v0.6.0 h1:SwM8C4hlK/YNLsdcXStfnHWE2HKkuTVwy5FKQHt5ro8= github.com/jba/templatecheck v0.6.0/go.mod h1:/1k7EajoSErFI9GLHAsiIJEaNLt3ALKNw2TV7z2SYv4= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y= golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20231114163143-69313e640400 h1:brbkEFfGwNGAEkykUOcryE/JiHUMMJouzE0fWWmz/QU= -golang.org/x/telemetry v0.0.0-20231114163143-69313e640400/go.mod h1:P6hMdmAcoG7FyATwqSr6R/U0n7yeXNP/QXeRlxb1szE= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509 h1:Nr7eTQpQZ/ytesxDJpQgaf0t4sdLnnDtAbmtViTrSUo= +golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509/go.mod h1:ZthVHHkOi8rlMEsfFr3Ie42Ym1NonbFNNRKW3ci0UrU= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78 h1:vcVnuftN4J4UKLRcgetjzfU9FjjgXUUYUc3JhFplgV4= +golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -45,15 +45,13 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/vuln v1.0.1 h1:KUas02EjQK5LTuIx1OylBQdKKZ9jeugs+HiqO5HormU= golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo= -honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k= -mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= -mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= -mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc= -mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg= +honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= +honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= +mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= +mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= +mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= diff --git a/gopls/internal/analysis/embeddirective/doc.go b/gopls/internal/analysis/embeddirective/doc.go index f8cc69fda77..bfed47f14f4 100644 --- a/gopls/internal/analysis/embeddirective/doc.go +++ b/gopls/internal/analysis/embeddirective/doc.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // Package embeddirective defines an Analyzer that validates //go:embed directives. -// The analyzer defers fixes to its parent source.Analyzer. +// The analyzer defers fixes to its parent golang.Analyzer. // // # Analyzer embed // diff --git a/gopls/internal/analysis/embeddirective/embeddirective.go b/gopls/internal/analysis/embeddirective/embeddirective.go index 20d3cbff5e4..2e415634338 100644 --- a/gopls/internal/analysis/embeddirective/embeddirective.go +++ b/gopls/internal/analysis/embeddirective/embeddirective.go @@ -26,9 +26,7 @@ var Analyzer = &analysis.Analyzer{ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/embeddirective", } -// source.fixedByImportingEmbed relies on this message to filter -// out fixable diagnostics from this Analyzer. -const MissingImportMessage = `must import "embed" when using go:embed directives` +const FixCategory = "addembedimport" // recognized by gopls ApplyFix func run(pass *analysis.Pass) (interface{}, error) { for _, f := range pass.Files { @@ -46,28 +44,39 @@ func run(pass *analysis.Pass) (interface{}, error) { } for _, c := range comments { - report := func(msg string) { - pass.Report(analysis.Diagnostic{ - Pos: c.Pos(), - End: c.Pos() + token.Pos(len("//go:embed")), - Message: msg, - }) - } + pos, end := c.Pos(), c.Pos()+token.Pos(len("//go:embed")) if !hasEmbedImport { - report(MissingImportMessage) + pass.Report(analysis.Diagnostic{ + Pos: pos, + End: end, + Message: `must import "embed" when using go:embed directives`, + Category: FixCategory, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: `Add missing "embed" import`, + // No TextEdits => computed by a gopls command. + }}, + }) } + var msg string spec := nextVarSpec(c, f) switch { case spec == nil: - report(`go:embed directives must precede a "var" declaration`) + msg = `go:embed directives must precede a "var" declaration` case len(spec.Names) != 1: - report("declarations following go:embed directives must define a single variable") + msg = "declarations following go:embed directives must define a single variable" case len(spec.Values) > 0: - report("declarations following go:embed directives must not specify a value") + msg = "declarations following go:embed directives must not specify a value" case !embeddableType(pass.TypesInfo.Defs[spec.Names[0]]): - report("declarations following go:embed directives must be of type string, []byte or embed.FS") + msg = "declarations following go:embed directives must be of type string, []byte or embed.FS" + } + if msg != "" { + pass.Report(analysis.Diagnostic{ + Pos: pos, + End: end, + Message: msg, + }) } } } diff --git a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go index e2337a111c8..1f8183fdcfa 100644 --- a/gopls/internal/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/analysis/fillstruct/fillstruct.go @@ -23,7 +23,6 @@ import ( "unicode" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/util/safetoken" @@ -31,41 +30,14 @@ import ( "golang.org/x/tools/internal/fuzzy" ) -const Doc = `note incomplete struct initializations - -This analyzer provides diagnostics for any struct literals that do not have -any fields initialized. Because the suggested fix for this analysis is -expensive to compute, callers should compute it separately, using the -SuggestedFix function below. -` - -var Analyzer = &analysis.Analyzer{ - Name: "fillstruct", - Doc: Doc, - Requires: []*analysis.Analyzer{inspect.Analyzer}, - Run: run, - URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillstruct", - RunDespiteErrors: true, -} - -// TODO(rfindley): remove this thin wrapper around the fillstruct refactoring, -// and eliminate the fillstruct analyzer. +// Diagnose computes diagnostics for fillable struct literals overlapping with +// the provided start and end position. // -// Previous iterations used the analysis framework for computing refactorings, -// which proved inefficient. -func run(pass *analysis.Pass) (interface{}, error) { - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - for _, d := range DiagnoseFillableStructs(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { - pass.Report(d) - } - return nil, nil -} - -// DiagnoseFillableStructs computes diagnostics for fillable struct composite -// literals overlapping with the provided start and end position. +// The diagnostic contains a lazy fix; the actual patch is computed +// (via the ApplyFix command) by a call to [SuggestedFix]. // -// If either start or end is invalid, it is considered an unbounded condition. -func DiagnoseFillableStructs(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { +// If either start or end is invalid, the entire package is inspected. +func Diagnose(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { var diags []analysis.Diagnostic nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { @@ -130,18 +102,25 @@ func DiagnoseFillableStructs(inspect *inspector.Inspector, start, end token.Pos, if i < totalFields { fillableFields = append(fillableFields, "...") } - name = fmt.Sprintf("anonymous struct { %s }", strings.Join(fillableFields, ", ")) + name = fmt.Sprintf("anonymous struct{ %s }", strings.Join(fillableFields, ", ")) } diags = append(diags, analysis.Diagnostic{ - Message: fmt.Sprintf("Fill %s", name), - Pos: expr.Pos(), - End: expr.End(), + Message: fmt.Sprintf("%s literal has missing fields", name), + Pos: expr.Pos(), + End: expr.End(), + Category: FixCategory, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Fill %s", name), + // No TextEdits => computed later by gopls. + }}, }) }) return diags } +const FixCategory = "fillstruct" // recognized by gopls ApplyFix + // SuggestedFix computes the suggested fix for the kinds of // diagnostics produced by the Analyzer above. func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { diff --git a/gopls/internal/analysis/fillstruct/fillstruct_test.go b/gopls/internal/analysis/fillstruct/fillstruct_test.go index 39fc81fee75..f90998fa459 100644 --- a/gopls/internal/analysis/fillstruct/fillstruct_test.go +++ b/gopls/internal/analysis/fillstruct/fillstruct_test.go @@ -5,13 +5,34 @@ package fillstruct_test import ( + "go/token" "testing" + "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/analysis/fillstruct" ) +// analyzer allows us to test the fillstruct code action using the analysistest +// harness. (fillstruct used to be a gopls analyzer.) +var analyzer = &analysis.Analyzer{ + Name: "fillstruct", + Doc: "test only", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: func(pass *analysis.Pass) (any, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + for _, d := range fillstruct.Diagnose(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { + pass.Report(d) + } + return nil, nil + }, + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillstruct", + RunDespiteErrors: true, +} + func Test(t *testing.T) { testdata := analysistest.TestData() - analysistest.Run(t, testdata, fillstruct.Analyzer, "a", "typeparams") + analysistest.Run(t, testdata, analyzer, "a", "typeparams") } diff --git a/gopls/internal/analysis/fillstruct/testdata/src/a/a.go b/gopls/internal/analysis/fillstruct/testdata/src/a/a.go index 9ee3860fcae..79c51d209c1 100644 --- a/gopls/internal/analysis/fillstruct/testdata/src/a/a.go +++ b/gopls/internal/analysis/fillstruct/testdata/src/a/a.go @@ -19,16 +19,16 @@ type basicStruct struct { foo int } -var _ = basicStruct{} // want `Fill basicStruct` +var _ = basicStruct{} // want `basicStruct literal has missing fields` type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} // want `Fill twoArgStruct` +var _ = twoArgStruct{} // want `twoArgStruct literal has missing fields` -var _ = twoArgStruct{ // want `Fill twoArgStruct` +var _ = twoArgStruct{ // want `twoArgStruct literal has missing fields` bar: "bar", } @@ -37,9 +37,9 @@ type nestedStruct struct { basic basicStruct } -var _ = nestedStruct{} // want `Fill nestedStruct` +var _ = nestedStruct{} // want `nestedStruct literal has missing fields` -var _ = data.B{} // want `Fill b.B` +var _ = data.B{} // want `b.B literal has missing fields` type typedStruct struct { m map[string]int @@ -49,25 +49,25 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} // want `Fill typedStruct` +var _ = typedStruct{} // want `typedStruct literal has missing fields` type funStruct struct { fn func(i int) int } -var _ = funStruct{} // want `Fill funStruct` +var _ = funStruct{} // want `funStruct literal has missing fields` type funStructComplex struct { fn func(i int, s string) (string, int) } -var _ = funStructComplex{} // want `Fill funStructComplex` +var _ = funStructComplex{} // want `funStructComplex literal has missing fields` type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} // want `Fill funStructEmpty` +var _ = funStructEmpty{} // want `funStructEmpty literal has missing fields` type Foo struct { A int @@ -78,7 +78,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} // want `Fill Bar` +var _ = Bar{} // want `Bar literal has missing fields` type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -89,7 +89,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} // want `Fill importedStruct` +var _ = importedStruct{} // want `importedStruct literal has missing fields` type pointerBuiltinStruct struct { b *bool @@ -97,17 +97,16 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} // want `Fill pointerBuiltinStruct` +var _ = pointerBuiltinStruct{} // want `pointerBuiltinStruct literal has missing fields` var _ = []ast.BasicLit{ - {}, // want `Fill go/ast.BasicLit` + {}, // want `go/ast.BasicLit literal has missing fields` } -var _ = []ast.BasicLit{{}, // want "go/ast.BasicLit" -} +var _ = []ast.BasicLit{{}} // want "go/ast.BasicLit literal has missing fields" type unsafeStruct struct { foo unsafe.Pointer } -var _ = unsafeStruct{} // want `Fill unsafeStruct` +var _ = unsafeStruct{} // want `unsafeStruct literal has missing fields` diff --git a/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go b/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go index 46bb8ae4027..d9e3da44a6f 100644 --- a/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go +++ b/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go @@ -12,16 +12,16 @@ type basicStruct[T any] struct { foo T } -var _ = basicStruct[int]{} // want `Fill basicStruct\[int\]` +var _ = basicStruct[int]{} // want `basicStruct\[int\] literal has missing fields` type twoArgStruct[F, B any] struct { foo F bar B } -var _ = twoArgStruct[string, int]{} // want `Fill twoArgStruct\[string, int\]` +var _ = twoArgStruct[string, int]{} // want `twoArgStruct\[string, int\] literal has missing fields` -var _ = twoArgStruct[int, string]{ // want `Fill twoArgStruct\[int, string\]` +var _ = twoArgStruct[int, string]{ // want `twoArgStruct\[int, string\] literal has missing fields` bar: "bar", } @@ -30,11 +30,11 @@ type nestedStruct struct { basic basicStruct[int] } -var _ = nestedStruct{} // want "Fill nestedStruct" +var _ = nestedStruct{} // want "nestedStruct literal has missing fields" func _[T any]() { type S struct{ t T } - x := S{} // want "Fill S" + x := S{} // want "S" _ = x } @@ -42,7 +42,7 @@ func Test() { var tests = []struct { a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p string }{ - {}, // want "Fill anonymous struct { a: string, b: string, c: string, ... }" + {}, // want "anonymous struct{ a: string, b: string, c: string, ... } literal has missing fields" } for _, test := range tests { _ = test diff --git a/gopls/internal/analysis/infertypeargs/infertypeargs.go b/gopls/internal/analysis/infertypeargs/infertypeargs.go index 2c3fdd31a94..9a514ad620c 100644 --- a/gopls/internal/analysis/infertypeargs/infertypeargs.go +++ b/gopls/internal/analysis/infertypeargs/infertypeargs.go @@ -2,16 +2,18 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package infertypeargs defines an analyzer that checks for explicit function -// arguments that could be inferred. package infertypeargs import ( + "go/ast" "go/token" + "go/types" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/versions" ) const Doc = `check for unnecessary type arguments in call expressions @@ -34,15 +36,114 @@ var Analyzer = &analysis.Analyzer{ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/infertypeargs", } -// TODO(rfindley): remove this thin wrapper around the infertypeargs refactoring, -// and eliminate the infertypeargs analyzer. -// -// Previous iterations used the analysis framework for computing refactorings, -// which proved inefficient. -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - for _, diag := range DiagnoseInferableTypeArgs(pass.Fset, inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { + for _, diag := range diagnose(pass.Fset, inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { pass.Report(diag) } return nil, nil } + +// Diagnose reports diagnostics describing simplifications to type +// arguments overlapping with the provided start and end position. +// +// If start or end is token.NoPos, the corresponding bound is not checked +// (i.e. if both start and end are NoPos, all call expressions are considered). +func diagnose(fset *token.FileSet, inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { + var diags []analysis.Diagnostic + + nodeFilter := []ast.Node{(*ast.CallExpr)(nil)} + inspect.Preorder(nodeFilter, func(node ast.Node) { + call := node.(*ast.CallExpr) + x, lbrack, indices, rbrack := typeparams.UnpackIndexExpr(call.Fun) + ident := calledIdent(x) + if ident == nil || len(indices) == 0 { + return // no explicit args, nothing to do + } + + if (start.IsValid() && call.End() < start) || (end.IsValid() && call.Pos() > end) { + return // non-overlapping + } + + // Confirm that instantiation actually occurred at this ident. + idata, ok := info.Instances[ident] + if !ok { + return // something went wrong, but fail open + } + instance := idata.Type + + // Start removing argument expressions from the right, and check if we can + // still infer the call expression. + required := len(indices) // number of type expressions that are required + for i := len(indices) - 1; i >= 0; i-- { + var fun ast.Expr + if i == 0 { + // No longer an index expression: just use the parameterized operand. + fun = x + } else { + fun = typeparams.PackIndexExpr(x, lbrack, indices[:i], indices[i-1].End()) + } + newCall := &ast.CallExpr{ + Fun: fun, + Lparen: call.Lparen, + Args: call.Args, + Ellipsis: call.Ellipsis, + Rparen: call.Rparen, + } + info := &types.Info{ + Instances: make(map[*ast.Ident]types.Instance), + } + versions.InitFileVersions(info) + if err := types.CheckExpr(fset, pkg, call.Pos(), newCall, info); err != nil { + // Most likely inference failed. + break + } + newIData := info.Instances[ident] + newInstance := newIData.Type + if !types.Identical(instance, newInstance) { + // The inferred result type does not match the original result type, so + // this simplification is not valid. + break + } + required = i + } + if required < len(indices) { + var s, e token.Pos + var edit analysis.TextEdit + if required == 0 { + s, e = lbrack, rbrack+1 // erase the entire index + edit = analysis.TextEdit{Pos: s, End: e} + } else { + s = indices[required].Pos() + e = rbrack + // erase from end of last arg to include last comma & white-spaces + edit = analysis.TextEdit{Pos: indices[required-1].End(), End: e} + } + // Recheck that our (narrower) fixes overlap with the requested range. + if (start.IsValid() && e < start) || (end.IsValid() && s > end) { + return // non-overlapping + } + diags = append(diags, analysis.Diagnostic{ + Pos: s, + End: e, + Message: "unnecessary type arguments", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Simplify type arguments", + TextEdits: []analysis.TextEdit{edit}, + }}, + }) + } + }) + + return diags +} + +func calledIdent(x ast.Expr) *ast.Ident { + switch x := x.(type) { + case *ast.Ident: + return x + case *ast.SelectorExpr: + return x.Sel + } + return nil +} diff --git a/gopls/internal/analysis/infertypeargs/run_go117.go b/gopls/internal/analysis/infertypeargs/run_go117.go deleted file mode 100644 index fdf831830dd..00000000000 --- a/gopls/internal/analysis/infertypeargs/run_go117.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.18 -// +build !go1.18 - -package infertypeargs - -import ( - "go/token" - "go/types" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/ast/inspector" -) - -// DiagnoseInferableTypeArgs returns an empty slice, as generics are not supported at -// this go version. -func DiagnoseInferableTypeArgs(fset *token.FileSet, inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { - return nil -} diff --git a/gopls/internal/analysis/infertypeargs/run_go118.go b/gopls/internal/analysis/infertypeargs/run_go118.go deleted file mode 100644 index b3fff4e8408..00000000000 --- a/gopls/internal/analysis/infertypeargs/run_go118.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -package infertypeargs - -import ( - "go/ast" - "go/token" - "go/types" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/internal/typeparams" - "golang.org/x/tools/internal/versions" -) - -// DiagnoseInferableTypeArgs reports diagnostics describing simplifications to type -// arguments overlapping with the provided start and end position. -// -// If start or end is token.NoPos, the corresponding bound is not checked -// (i.e. if both start and end are NoPos, all call expressions are considered). -func DiagnoseInferableTypeArgs(fset *token.FileSet, inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { - var diags []analysis.Diagnostic - - nodeFilter := []ast.Node{(*ast.CallExpr)(nil)} - inspect.Preorder(nodeFilter, func(node ast.Node) { - call := node.(*ast.CallExpr) - x, lbrack, indices, rbrack := typeparams.UnpackIndexExpr(call.Fun) - ident := calledIdent(x) - if ident == nil || len(indices) == 0 { - return // no explicit args, nothing to do - } - - if (start.IsValid() && call.End() < start) || (end.IsValid() && call.Pos() > end) { - return // non-overlapping - } - - // Confirm that instantiation actually occurred at this ident. - idata, ok := info.Instances[ident] - if !ok { - return // something went wrong, but fail open - } - instance := idata.Type - - // Start removing argument expressions from the right, and check if we can - // still infer the call expression. - required := len(indices) // number of type expressions that are required - for i := len(indices) - 1; i >= 0; i-- { - var fun ast.Expr - if i == 0 { - // No longer an index expression: just use the parameterized operand. - fun = x - } else { - fun = typeparams.PackIndexExpr(x, lbrack, indices[:i], indices[i-1].End()) - } - newCall := &ast.CallExpr{ - Fun: fun, - Lparen: call.Lparen, - Args: call.Args, - Ellipsis: call.Ellipsis, - Rparen: call.Rparen, - } - info := &types.Info{ - Instances: make(map[*ast.Ident]types.Instance), - } - versions.InitFileVersions(info) - if err := types.CheckExpr(fset, pkg, call.Pos(), newCall, info); err != nil { - // Most likely inference failed. - break - } - newIData := info.Instances[ident] - newInstance := newIData.Type - if !types.Identical(instance, newInstance) { - // The inferred result type does not match the original result type, so - // this simplification is not valid. - break - } - required = i - } - if required < len(indices) { - var s, e token.Pos - var edit analysis.TextEdit - if required == 0 { - s, e = lbrack, rbrack+1 // erase the entire index - edit = analysis.TextEdit{Pos: s, End: e} - } else { - s = indices[required].Pos() - e = rbrack - // erase from end of last arg to include last comma & white-spaces - edit = analysis.TextEdit{Pos: indices[required-1].End(), End: e} - } - // Recheck that our (narrower) fixes overlap with the requested range. - if (start.IsValid() && e < start) || (end.IsValid() && s > end) { - return // non-overlapping - } - diags = append(diags, analysis.Diagnostic{ - Pos: s, - End: e, - Message: "unnecessary type arguments", - SuggestedFixes: []analysis.SuggestedFix{{ - Message: "simplify type arguments", - TextEdits: []analysis.TextEdit{edit}, - }}, - }) - } - }) - - return diags -} - -func calledIdent(x ast.Expr) *ast.Ident { - switch x := x.(type) { - case *ast.Ident: - return x - case *ast.SelectorExpr: - return x.Sel - } - return nil -} diff --git a/gopls/internal/analysis/stubmethods/doc.go b/gopls/internal/analysis/stubmethods/doc.go index 5b5957d2c69..e1383cfc7e7 100644 --- a/gopls/internal/analysis/stubmethods/doc.go +++ b/gopls/internal/analysis/stubmethods/doc.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. -// Package stubmethods defines an analyzer for missing interface methods. +// Package stubmethods defines a code action for missing interface methods. // // # Analyzer stubmethods // @@ -34,5 +34,5 @@ // // (At least, it appears to behave that way, but technically it // doesn't use the SuggestedFix mechanism and the stub is created by -// logic in gopls's source.stub function.) +// logic in gopls's golang.stub function.) package stubmethods diff --git a/gopls/internal/analysis/stubmethods/stubmethods.go b/gopls/internal/analysis/stubmethods/stubmethods.go index 02eef5c29c1..1215be840d0 100644 --- a/gopls/internal/analysis/stubmethods/stubmethods.go +++ b/gopls/internal/analysis/stubmethods/stubmethods.go @@ -12,11 +12,11 @@ import ( "go/format" "go/token" "go/types" - "strconv" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/typesinternal" ) @@ -73,9 +73,6 @@ func MatchesMessage(msg string) bool { // interface to fix the type checking error defined by (start, end, msg). // // If no such fix is possible, the second result is false. -// -// TODO(rfindley): simplify this signature once the stubmethods refactoring is -// no longer wedged into the analysis framework. func DiagnosticForError(fset *token.FileSet, file *ast.File, start, end token.Pos, msg string, info *types.Info) (analysis.Diagnostic, bool) { if !MatchesMessage(msg) { return analysis.Diagnostic{}, false @@ -86,14 +83,22 @@ func DiagnosticForError(fset *token.FileSet, file *ast.File, start, end token.Po if si == nil { return analysis.Diagnostic{}, false } - qf := RelativeToFiles(si.Concrete.Obj().Pkg(), file, nil, nil) + qf := typesutil.FileQualifier(file, si.Concrete.Obj().Pkg(), info) + iface := types.TypeString(si.Interface.Type(), qf) return analysis.Diagnostic{ - Pos: start, - End: end, - Message: fmt.Sprintf("Implement %s", types.TypeString(si.Interface.Type(), qf)), + Pos: start, + End: end, + Message: msg, + Category: FixCategory, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Declare missing methods of %s", iface), + // No TextEdits => computed later by gopls. + }}, }, true } +const FixCategory = "stubmethods" // recognized by gopls ApplyFix + // StubInfo represents a concrete type // that wants to stub out an interface type type StubInfo struct { @@ -328,71 +333,6 @@ func fromAssignStmt(fset *token.FileSet, info *types.Info, assign *ast.AssignStm } } -// RelativeToFiles returns a types.Qualifier that formats package -// names according to the import environments of the files that define -// the concrete type and the interface type. (Only the imports of the -// latter file are provided.) -// -// This is similar to types.RelativeTo except if a file imports the package with a different name, -// then it will use it. And if the file does import the package but it is ignored, -// then it will return the original name. It also prefers package names in importEnv in case -// an import is missing from concFile but is present among importEnv. -// -// Additionally, if missingImport is not nil, the function will be called whenever the concFile -// is presented with a package that is not imported. This is useful so that as types.TypeString is -// formatting a function signature, it is identifying packages that will need to be imported when -// stubbing an interface. -// -// TODO(rfindley): investigate if this can be merged with source.Qualifier. -func RelativeToFiles(concPkg *types.Package, concFile *ast.File, ifaceImports []*ast.ImportSpec, missingImport func(name, path string)) types.Qualifier { - return func(other *types.Package) string { - if other == concPkg { - return "" - } - - // Check if the concrete file already has the given import, - // if so return the default package name or the renamed import statement. - for _, imp := range concFile.Imports { - impPath, _ := strconv.Unquote(imp.Path.Value) - isIgnored := imp.Name != nil && (imp.Name.Name == "." || imp.Name.Name == "_") - // TODO(adonovan): this comparison disregards a vendor prefix in 'other'. - if impPath == other.Path() && !isIgnored { - importName := other.Name() - if imp.Name != nil { - importName = imp.Name.Name - } - return importName - } - } - - // If the concrete file does not have the import, check if the package - // is renamed in the interface file and prefer that. - var importName string - for _, imp := range ifaceImports { - impPath, _ := strconv.Unquote(imp.Path.Value) - isIgnored := imp.Name != nil && (imp.Name.Name == "." || imp.Name.Name == "_") - // TODO(adonovan): this comparison disregards a vendor prefix in 'other'. - if impPath == other.Path() && !isIgnored { - if imp.Name != nil && imp.Name.Name != concPkg.Name() { - importName = imp.Name.Name - } - break - } - } - - if missingImport != nil { - missingImport(importName, other.Path()) - } - - // Up until this point, importName must stay empty when calling missingImport, - // otherwise we'd end up with `import time "time"` which doesn't look idiomatic. - if importName == "" { - importName = other.Name() - } - return importName - } -} - // ifaceType returns the named interface type to which e refers, if any. func ifaceType(e ast.Expr, info *types.Info) *types.TypeName { tv, ok := info.Types[e] diff --git a/gopls/internal/analysis/undeclaredname/undeclared.go b/gopls/internal/analysis/undeclaredname/undeclared.go index 377c635a5b7..10393d4a86e 100644 --- a/gopls/internal/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/analysis/undeclaredname/undeclared.go @@ -44,6 +44,7 @@ func run(pass *analysis.Pass) (interface{}, error) { } func runForError(pass *analysis.Pass, err types.Error) { + // Extract symbol name from error. var name string for _, prefix := range undeclaredNamePrefixes { if !strings.HasPrefix(err.Msg, prefix) { @@ -54,6 +55,8 @@ func runForError(pass *analysis.Pass, err types.Error) { if name == "" { return } + + // Find file enclosing error. var file *ast.File for _, f := range pass.Files { if f.Pos() <= err.Pos && err.Pos < f.End() { @@ -65,7 +68,7 @@ func runForError(pass *analysis.Pass, err types.Error) { return } - // Get the path for the relevant range. + // Find path to identifier in the error. path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos) if len(path) < 2 { return @@ -75,6 +78,12 @@ func runForError(pass *analysis.Pass, err types.Error) { return } + // Skip selector expressions because it might be too complex + // to try and provide a suggested fix for fields and methods. + if _, ok := path[1].(*ast.SelectorExpr); ok { + return + } + // Undeclared quick fixes only work in function bodies. inFunc := false for i := range path { @@ -91,24 +100,27 @@ func runForError(pass *analysis.Pass, err types.Error) { if !inFunc { return } - // Skip selector expressions because it might be too complex - // to try and provide a suggested fix for fields and methods. - if _, ok := path[1].(*ast.SelectorExpr); ok { - return - } - tok := pass.Fset.File(file.Pos()) - if tok == nil { - return + + // Offer a fix. + noun := "variable" + if isCallPosition(path) { + noun = "function" } - offset := safetoken.StartPosition(pass.Fset, err.Pos).Offset - end := tok.Pos(offset + len(name)) // TODO(adonovan): dubious! err.Pos + len(name)?? pass.Report(analysis.Diagnostic{ - Pos: err.Pos, - End: end, - Message: err.Msg, + Pos: err.Pos, + End: err.Pos + token.Pos(len(name)), + Message: err.Msg, + Category: FixCategory, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Create %s %q", noun, name), + // No TextEdits => computed by a gopls command + }}, }) } +const FixCategory = "undeclaredname" // recognized by gopls ApplyFix + +// SuggestedFix computes the edits for the lazy (no-edits) fix suggested by the analyzer. func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { pos := start // don't use the end path, _ := astutil.PathEnclosingInterval(file, pos, pos) @@ -122,10 +134,8 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil // Check for a possible call expression, in which case we should add a // new function declaration. - if len(path) > 1 { - if _, ok := path[1].(*ast.CallExpr); ok { - return newFunctionDeclaration(path, file, pkg, info, fset) - } + if isCallPosition(path) { + return newFunctionDeclaration(path, file, pkg, info, fset) } // Get the place to insert the new statement. @@ -146,7 +156,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil // Create the new local variable statement. newStmt := fmt.Sprintf("%s := %s", ident.Name, indent) return fset, &analysis.SuggestedFix{ - Message: fmt.Sprintf("Create variable \"%s\"", ident.Name), + Message: fmt.Sprintf("Create variable %q", ident.Name), TextEdits: []analysis.TextEdit{{ Pos: insertBeforeStmt.Pos(), End: insertBeforeStmt.Pos(), @@ -271,7 +281,8 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, Name: ast.NewIdent(ident.Name), Type: &ast.FuncType{ Params: params, - // TODO(rstambler): Also handle result parameters here. + // TODO(golang/go#47558): Also handle result + // parameters here based on context of CallExpr. }, Body: &ast.BlockStmt{ List: []ast.Stmt{ @@ -294,7 +305,7 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, return nil, nil, err } return fset, &analysis.SuggestedFix{ - Message: fmt.Sprintf("Create function \"%s\"", ident.Name), + Message: fmt.Sprintf("Create function %q", ident.Name), TextEdits: []analysis.TextEdit{{ Pos: pos, End: pos, @@ -302,6 +313,7 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, }}, }, nil } + func typeToArgName(ty types.Type) string { s := types.Default(ty).String() @@ -333,3 +345,15 @@ func typeToArgName(ty types.Type) string { a[0] = unicode.ToLower(a[0]) return string(a) } + +// isCallPosition reports whether the path denotes the subtree in call position, f(). +func isCallPosition(path []ast.Node) bool { + return len(path) > 1 && + is[*ast.CallExpr](path[1]) && + path[1].(*ast.CallExpr).Fun == path[0] +} + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} diff --git a/gopls/internal/analysis/unusedparams/cmd/main.go b/gopls/internal/analysis/unusedparams/cmd/main.go index 2355e3c4b52..2f35fb06083 100644 --- a/gopls/internal/analysis/unusedparams/cmd/main.go +++ b/gopls/internal/analysis/unusedparams/cmd/main.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. -// The stringintconv command runs the stringintconv analyzer. +// The unusedparams command runs the unusedparams analyzer. package main import ( diff --git a/gopls/internal/analysis/unusedparams/doc.go b/gopls/internal/analysis/unusedparams/doc.go index 2bf9fa96cf3..07e43c0d084 100644 --- a/gopls/internal/analysis/unusedparams/doc.go +++ b/gopls/internal/analysis/unusedparams/doc.go @@ -12,9 +12,23 @@ // The unusedparams analyzer checks functions to see if there are // any parameters that are not being used. // -// To reduce false positives it ignores: -// - methods -// - parameters that do not have a name or have the name '_' (the blank identifier) -// - functions in test files -// - functions with empty bodies or those with just a return stmt +// To ensure soundness, it ignores: +// - "address-taken" functions, that is, functions that are used as +// a value rather than being called directly; their signatures may +// be required to conform to a func type. +// - exported functions or methods, since they may be address-taken +// in another package. +// - unexported methods whose name matches an interface method +// declared in the same package, since the method's signature +// may be required to conform to the interface type. +// - functions with empty bodies, or containing just a call to panic. +// - parameters that are unnamed, or named "_", the blank identifier. +// +// The analyzer suggests a fix of replacing the parameter name by "_", +// but in such cases a deeper fix can be obtained by invoking the +// "Refactor: remove unused parameter" code action, which will +// eliminate the parameter entirely, along with all corresponding +// arguments at call sites, while taking care to preserve any side +// effects in the argument expressions; see +// https://github.com/golang/tools/releases/tag/gopls%2Fv0.14. package unusedparams diff --git a/gopls/internal/analysis/unusedparams/testdata/src/a/a.go b/gopls/internal/analysis/unusedparams/testdata/src/a/a.go index 23e4122c4cc..3661e1f3cbe 100644 --- a/gopls/internal/analysis/unusedparams/testdata/src/a/a.go +++ b/gopls/internal/analysis/unusedparams/testdata/src/a/a.go @@ -24,28 +24,28 @@ func (y *yuh) n(f bool) { } } -func a(i1 int, i2 int, i3 int) int { // want "potentially unused parameter: 'i2'" +func a(i1 int, i2 int, i3 int) int { // want "unused parameter: i2" i3 += i1 - _ = func(z int) int { // want "potentially unused parameter: 'z'" + _ = func(z int) int { // want "unused parameter: z" _ = 1 return 1 } return i3 } -func b(c bytes.Buffer) { // want "potentially unused parameter: 'c'" +func b(c bytes.Buffer) { // want "unused parameter: c" _ = 1 } -func z(h http.ResponseWriter, _ *http.Request) { // want "potentially unused parameter: 'h'" +func z(h http.ResponseWriter, _ *http.Request) { // no report: func z is address-taken fmt.Println("Before") } -func l(h http.Handler) http.Handler { +func l(h http.Handler) http.Handler { // want "unused parameter: h" return http.HandlerFunc(z) } -func mult(a, b int) int { // want "potentially unused parameter: 'b'" +func mult(a, b int) int { // want "unused parameter: b" a += 1 return a } @@ -53,3 +53,35 @@ func mult(a, b int) int { // want "potentially unused parameter: 'b'" func y(a int) { panic("yo") } + +var _ = func(x int) {} // empty body: no diagnostic + +var _ = func(x int) { println() } // want "unused parameter: x" + +var ( + calledGlobal = func(x int) { println() } // want "unused parameter: x" + addressTakenGlobal = func(x int) { println() } // no report: function is address-taken +) + +func _() { + calledGlobal(1) + println(addressTakenGlobal) +} + +func Exported(unused int) {} // no finding: an exported function may be address-taken + +type T int + +func (T) m(f bool) { println() } // want "unused parameter: f" +func (T) n(f bool) { println() } // no finding: n may match the interface method parent.n + +func _() { + var fib func(x, y int) int + fib = func(x, y int) int { // want "unused parameter: y" + if x < 2 { + return x + } + return fib(x-1, 123) + fib(x-2, 456) + } + fib(10, 42) +} diff --git a/gopls/internal/analysis/unusedparams/testdata/src/a/a.go.golden b/gopls/internal/analysis/unusedparams/testdata/src/a/a.go.golden index e28a6bdeabe..dea8a6d44ae 100644 --- a/gopls/internal/analysis/unusedparams/testdata/src/a/a.go.golden +++ b/gopls/internal/analysis/unusedparams/testdata/src/a/a.go.golden @@ -24,28 +24,28 @@ func (y *yuh) n(f bool) { } } -func a(i1 int, _ int, i3 int) int { // want "potentially unused parameter: 'i2'" +func a(i1 int, _ int, i3 int) int { // want "unused parameter: i2" i3 += i1 - _ = func(_ int) int { // want "potentially unused parameter: 'z'" + _ = func(_ int) int { // want "unused parameter: z" _ = 1 return 1 } return i3 } -func b(_ bytes.Buffer) { // want "potentially unused parameter: 'c'" +func b(_ bytes.Buffer) { // want "unused parameter: c" _ = 1 } -func z(_ http.ResponseWriter, _ *http.Request) { // want "potentially unused parameter: 'h'" +func z(h http.ResponseWriter, _ *http.Request) { // no report: func z is address-taken fmt.Println("Before") } -func l(h http.Handler) http.Handler { +func l(_ http.Handler) http.Handler { // want "unused parameter: h" return http.HandlerFunc(z) } -func mult(a, _ int) int { // want "potentially unused parameter: 'b'" +func mult(a, _ int) int { // want "unused parameter: b" a += 1 return a } @@ -53,3 +53,35 @@ func mult(a, _ int) int { // want "potentially unused parameter: 'b'" func y(a int) { panic("yo") } + +var _ = func(x int) {} // empty body: no diagnostic + +var _ = func(_ int) { println() } // want "unused parameter: x" + +var ( + calledGlobal = func(_ int) { println() } // want "unused parameter: x" + addressTakenGlobal = func(x int) { println() } // no report: function is address-taken +) + +func _() { + calledGlobal(1) + println(addressTakenGlobal) +} + +func Exported(unused int) {} // no finding: an exported function may be address-taken + +type T int + +func (T) m(_ bool) { println() } // want "unused parameter: f" +func (T) n(f bool) { println() } // no finding: n may match the interface method parent.n + +func _() { + var fib func(x, y int) int + fib = func(x, _ int) int { // want "unused parameter: y" + if x < 2 { + return x + } + return fib(x-1, 123) + fib(x-2, 456) + } + fib(10, 42) +} diff --git a/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go b/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go index 93af2681b94..d89926a7db5 100644 --- a/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go +++ b/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go @@ -24,28 +24,28 @@ func (y *yuh[int]) n(f bool) { } } -func a[T comparable](i1 int, i2 T, i3 int) int { // want "potentially unused parameter: 'i2'" +func a[T comparable](i1 int, i2 T, i3 int) int { // want "unused parameter: i2" i3 += i1 - _ = func(z int) int { // want "potentially unused parameter: 'z'" + _ = func(z int) int { // want "unused parameter: z" _ = 1 return 1 } return i3 } -func b[T any](c bytes.Buffer) { // want "potentially unused parameter: 'c'" +func b[T any](c bytes.Buffer) { // want "unused parameter: c" _ = 1 } -func z[T http.ResponseWriter](h T, _ *http.Request) { // want "potentially unused parameter: 'h'" +func z[T http.ResponseWriter](h T, _ *http.Request) { // no report: func z is address-taken fmt.Println("Before") } -func l(h http.Handler) http.Handler { +func l(h http.Handler) http.Handler { // want "unused parameter: h" return http.HandlerFunc(z[http.ResponseWriter]) } -func mult(a, b int) int { // want "potentially unused parameter: 'b'" +func mult(a, b int) int { // want "unused parameter: b" a += 1 return a } diff --git a/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden index c86bf289a3e..85479bc8b50 100644 --- a/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden +++ b/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden @@ -24,28 +24,28 @@ func (y *yuh[int]) n(f bool) { } } -func a[T comparable](i1 int, _ T, i3 int) int { // want "potentially unused parameter: 'i2'" +func a[T comparable](i1 int, _ T, i3 int) int { // want "unused parameter: i2" i3 += i1 - _ = func(_ int) int { // want "potentially unused parameter: 'z'" + _ = func(_ int) int { // want "unused parameter: z" _ = 1 return 1 } return i3 } -func b[T any](_ bytes.Buffer) { // want "potentially unused parameter: 'c'" +func b[T any](_ bytes.Buffer) { // want "unused parameter: c" _ = 1 } -func z[T http.ResponseWriter](_ T, _ *http.Request) { // want "potentially unused parameter: 'h'" +func z[T http.ResponseWriter](h T, _ *http.Request) { // no report: func z is address-taken fmt.Println("Before") } -func l(h http.Handler) http.Handler { +func l(_ http.Handler) http.Handler { // want "unused parameter: h" return http.HandlerFunc(z[http.ResponseWriter]) } -func mult(a, _ int) int { // want "potentially unused parameter: 'b'" +func mult(a, _ int) int { // want "unused parameter: b" a += 1 return a } diff --git a/gopls/internal/analysis/unusedparams/unusedparams.go b/gopls/internal/analysis/unusedparams/unusedparams.go index 076acf79db6..74cd662285c 100644 --- a/gopls/internal/analysis/unusedparams/unusedparams.go +++ b/gopls/internal/analysis/unusedparams/unusedparams.go @@ -9,151 +9,300 @@ import ( "fmt" "go/ast" "go/types" - "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/analysisinternal" ) //go:embed doc.go var doc string -var ( - Analyzer = &analysis.Analyzer{ - Name: "unusedparams", - Doc: analysisinternal.MustExtractDoc(doc, "unusedparams"), - Requires: []*analysis.Analyzer{inspect.Analyzer}, - Run: run, - URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams", - } - inspectLits bool - inspectWrappers bool -) - -func init() { - Analyzer.Flags.BoolVar(&inspectLits, "lits", true, "inspect function literals") - Analyzer.Flags.BoolVar(&inspectWrappers, "wrappers", false, "inspect functions whose body consists of a single return statement") +var Analyzer = &analysis.Analyzer{ + Name: "unusedparams", + Doc: analysisinternal.MustExtractDoc(doc, "unusedparams"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams", } -type paramData struct { - field *ast.Field - ident *ast.Ident - typObj types.Object -} +const FixCategory = "unusedparam" // recognized by gopls ApplyFix -func run(pass *analysis.Pass) (interface{}, error) { +func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - nodeFilter := []ast.Node{ - (*ast.FuncDecl)(nil), - } - if inspectLits { - nodeFilter = append(nodeFilter, (*ast.FuncLit)(nil)) - } - inspect.Preorder(nodeFilter, func(n ast.Node) { - var fieldList *ast.FieldList - var body *ast.BlockStmt + // First find all "address-taken" functions. + // We must conservatively assume that their parameters + // are all required to conform to some signature. + // + // A named function is address-taken if it is somewhere + // used not in call position: + // + // f(...) // not address-taken + // use(f) // address-taken + // + // A literal function is address-taken if it is not + // immediately bound to a variable, or if that variable is + // used not in call position: + // + // f := func() { ... }; f() used only in call position + // var f func(); f = func() { ...f()... }; f() ditto + // use(func() { ... }) address-taken + // - // Get the fieldList and body from the function node. - switch f := n.(type) { - case *ast.FuncDecl: - fieldList, body = f.Type.Params, f.Body - // TODO(golang/go#36602): add better handling for methods, if we enable methods - // we will get false positives if a struct is potentially implementing - // an interface. - if f.Recv != nil { - return + // Note: this algorithm relies on the assumption that the + // analyzer is called only for the "widest" package for a + // given file: that is, p_test in preference to p, if both + // exist. Analyzing only package p may produce diagnostics + // that would be falsified based on declarations in p_test.go + // files. The gopls analysis driver does this, but most + // drivers to not, so running this command in, say, + // unitchecker or multichecker may produce incorrect results. + + // Gather global information: + // - uses of functions not in call position + // - unexported interface methods + // - all referenced variables + + usesOutsideCall := make(map[types.Object][]*ast.Ident) + unexportedIMethodNames := make(map[string]bool) + { + callPosn := make(map[*ast.Ident]bool) // all idents f appearing in f() calls + filter := []ast.Node{ + (*ast.CallExpr)(nil), + (*ast.InterfaceType)(nil), + } + inspect.Preorder(filter, func(n ast.Node) { + switch n := n.(type) { + case *ast.CallExpr: + // Strip off any generic instantiation. + fun := n.Fun + switch fun_ := fun.(type) { + case *ast.IndexExpr: + fun = fun_.X // f[T]() (funcs[i]() is rejected below) + case *ast.IndexListExpr: + fun = fun_.X // f[K, V]() + } + + // Find object: + // record non-exported function, method, or func-typed var. + var id *ast.Ident + switch fun := fun.(type) { + case *ast.Ident: + id = fun + case *ast.SelectorExpr: + id = fun.Sel + } + if id != nil && !id.IsExported() { + switch pass.TypesInfo.Uses[id].(type) { + case *types.Func, *types.Var: + callPosn[id] = true + } + } + + case *ast.InterfaceType: + // Record the set of names of unexported interface methods. + // (It would be more precise to record signatures but + // generics makes it tricky, and this conservative + // heuristic is close enough.) + t := pass.TypesInfo.TypeOf(n).(*types.Interface) + for i := 0; i < t.NumExplicitMethods(); i++ { + m := t.ExplicitMethod(i) + if !m.Exported() && m.Name() != "_" { + unexportedIMethodNames[m.Name()] = true + } + } } + }) - // Ignore functions in _test.go files to reduce false positives. - if file := pass.Fset.File(n.Pos()); file != nil && strings.HasSuffix(file.Name(), "_test.go") { - return + for id, obj := range pass.TypesInfo.Uses { + if !callPosn[id] { + // This includes "f = func() {...}", which we deal with below. + usesOutsideCall[obj] = append(usesOutsideCall[obj], id) } - case *ast.FuncLit: - fieldList, body = f.Type.Params, f.Body } - // If there are no arguments or the function is empty, then return. - if fieldList.NumFields() == 0 || body == nil || len(body.List) == 0 { - return + } + + // Find all vars (notably parameters) that are used. + usedVars := make(map[*types.Var]bool) + for _, obj := range pass.TypesInfo.Uses { + if v, ok := obj.(*types.Var); ok { + if v.IsField() { + continue // no point gathering these + } + usedVars[v] = true + } + } + + // Check each non-address-taken function's parameters are all used. + filter := []ast.Node{ + (*ast.FuncDecl)(nil), + (*ast.FuncLit)(nil), + } + inspect.WithStack(filter, func(n ast.Node, push bool, stack []ast.Node) bool { + // (We always return true so that we visit nested FuncLits.) + + if !push { + return true } - switch expr := body.List[0].(type) { - case *ast.ReturnStmt: - if !inspectWrappers { - // Ignore functions that only contain a return statement to reduce false positives. - return + var ( + fn types.Object // function symbol (*Func, possibly *Var for a FuncLit) + ftype *ast.FuncType + body *ast.BlockStmt + ) + switch n := n.(type) { + case *ast.FuncDecl: + // We can't analyze non-Go functions. + if n.Body == nil { + return true } - case *ast.ExprStmt: - callExpr, ok := expr.X.(*ast.CallExpr) - if !ok || len(body.List) > 1 { - break + + // Ignore exported functions and methods: we + // must assume they may be address-taken in + // another package. + if n.Name.IsExported() { + return true } - // Ignore functions that only contain a panic statement to reduce false positives. - if fun, ok := callExpr.Fun.(*ast.Ident); ok && fun.Name == "panic" { - return + + // Ignore methods that match the name of any + // interface method declared in this package, + // as the method's signature may need to conform + // to the interface. + if n.Recv != nil && unexportedIMethodNames[n.Name.Name] { + return true } - } - // Get the useful data from each field. - params := make(map[string]*paramData) - unused := make(map[*paramData]bool) - for _, f := range fieldList.List { - for _, i := range f.Names { - if i.Name == "_" { - continue + fn = pass.TypesInfo.Defs[n.Name].(*types.Func) + ftype, body = n.Type, n.Body + + case *ast.FuncLit: + // Find the symbol for the variable (if any) + // to which the FuncLit is bound. + // (We don't bother to allow ParenExprs.) + switch parent := stack[len(stack)-2].(type) { + case *ast.AssignStmt: + // f = func() {...} + // f := func() {...} + for i, rhs := range parent.Rhs { + if rhs == n { + if id, ok := parent.Lhs[i].(*ast.Ident); ok { + fn = pass.TypesInfo.ObjectOf(id) + + // Edge case: f = func() {...} + // should not count as a use. + if pass.TypesInfo.Uses[id] != nil { + usesOutsideCall[fn] = slices.Remove(usesOutsideCall[fn], id) + } + + if fn == nil && id.Name == "_" { + // Edge case: _ = func() {...} + // has no var. Fake one. + fn = types.NewVar(id.Pos(), pass.Pkg, id.Name, pass.TypesInfo.TypeOf(n)) + } + } + break + } } - params[i.Name] = ¶mData{ - field: f, - ident: i, - typObj: pass.TypesInfo.ObjectOf(i), + + case *ast.ValueSpec: + // var f = func() { ... } + // (unless f is an exported package-level var) + for i, val := range parent.Values { + if val == n { + v := pass.TypesInfo.Defs[parent.Names[i]] + if !(v.Parent() == pass.Pkg.Scope() && v.Exported()) { + fn = v + } + break + } } - unused[params[i.Name]] = true } + + ftype, body = n.Type, n.Body } - // Traverse through the body of the function and - // check to see which parameters are unused. - ast.Inspect(body, func(node ast.Node) bool { - n, ok := node.(*ast.Ident) - if !ok { - return true - } - param, ok := params[n.Name] - if !ok { - return false - } - if nObj := pass.TypesInfo.ObjectOf(n); nObj != param.typObj { - return false + // Ignore address-taken functions and methods: unused + // parameters may be needed to conform to a func type. + if fn == nil || len(usesOutsideCall[fn]) > 0 { + return true + } + + // If there are no parameters, there are no unused parameters. + if ftype.Params.NumFields() == 0 { + return true + } + + // To reduce false positives, ignore functions with an + // empty or panic body. + // + // We choose not to ignore functions whose body is a + // single return statement (as earlier versions did) + // func f() { return } + // func f() { return g(...) } + // as we suspect that was just heuristic to reduce + // false positives in the earlier unsound algorithm. + switch len(body.List) { + case 0: + // Empty body. Although the parameter is + // unnecessary, it's pretty obvious to the + // reader that that's the case, so we allow it. + return true // func f() {} + case 1: + if stmt, ok := body.List[0].(*ast.ExprStmt); ok { + // We allow a panic body, as it is often a + // placeholder for a future implementation: + // func f() { panic(...) } + if call, ok := stmt.X.(*ast.CallExpr); ok { + if fun, ok := call.Fun.(*ast.Ident); ok && fun.Name == "panic" { + return true + } + } } - delete(unused, param) - return false - }) + } - // Create the reports for the unused parameters. - for u := range unused { - start, end := u.field.Pos(), u.field.End() - if len(u.field.Names) > 1 { - start, end = u.ident.Pos(), u.ident.End() + // Report each unused parameter. + for _, field := range ftype.Params.List { + for _, id := range field.Names { + if id.Name == "_" { + continue + } + param := pass.TypesInfo.Defs[id].(*types.Var) + if !usedVars[param] { + start, end := field.Pos(), field.End() + if len(field.Names) > 1 { + start, end = id.Pos(), id.End() + } + // This diagnostic carries both an edit-based fix to + // rename the unused parameter, and a command-based fix + // to remove it (see golang.RemoveUnusedParameter). + pass.Report(analysis.Diagnostic{ + Pos: start, + End: end, + Message: fmt.Sprintf("unused parameter: %s", id.Name), + Category: FixCategory, + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: `Rename parameter to "_"`, + TextEdits: []analysis.TextEdit{{ + Pos: id.Pos(), + End: id.End(), + NewText: []byte("_"), + }}, + }, + { + Message: fmt.Sprintf("Remove unused parameter %q", id.Name), + // No TextEdits => computed by gopls command + }, + }, + }) + } } - // TODO(golang/go#36602): Add suggested fixes to automatically - // remove the unused parameter from every use of this - // function. - pass.Report(analysis.Diagnostic{ - Pos: start, - End: end, - Message: fmt.Sprintf("potentially unused parameter: '%s'", u.ident.Name), - SuggestedFixes: []analysis.SuggestedFix{{ - Message: `Replace with "_"`, - TextEdits: []analysis.TextEdit{{ - Pos: u.ident.Pos(), - End: u.ident.End(), - NewText: []byte("_"), - }}, - }}, - }) } + + return true }) return nil, nil } diff --git a/gopls/internal/analysis/unusedvariable/unusedvariable.go b/gopls/internal/analysis/unusedvariable/unusedvariable.go index e85b9dc84ee..f8a4db1d292 100644 --- a/gopls/internal/analysis/unusedvariable/unusedvariable.go +++ b/gopls/internal/analysis/unusedvariable/unusedvariable.go @@ -104,7 +104,7 @@ func runForError(pass *analysis.Pass, err types.Error, name string) error { continue } - fixes := removeVariableFromAssignment(pass, path, stmt, ident) + fixes := removeVariableFromAssignment(path, stmt, ident) // fixes may be nil if len(fixes) > 0 { diag.SuggestedFixes = fixes @@ -155,10 +155,14 @@ func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.Valu // Find parent DeclStmt and delete it for _, node := range path { if declStmt, ok := node.(*ast.DeclStmt); ok { + edits := deleteStmtFromBlock(path, declStmt) + if len(edits) == 0 { + return nil // can this happen? + } return []analysis.SuggestedFix{ { Message: suggestedFixMessage(ident.Name), - TextEdits: deleteStmtFromBlock(path, declStmt), + TextEdits: edits, }, } } @@ -185,7 +189,7 @@ func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.Valu } } -func removeVariableFromAssignment(pass *analysis.Pass, path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix { +func removeVariableFromAssignment(path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix { // The only variable in the assignment is unused if len(stmt.Lhs) == 1 { // If LHS has only one expression to be valid it has to have 1 expression @@ -208,10 +212,14 @@ func removeVariableFromAssignment(pass *analysis.Pass, path []ast.Node, stmt *as } // RHS does not have any side effects, delete the whole statement + edits := deleteStmtFromBlock(path, stmt) + if len(edits) == 0 { + return nil // can this happen? + } return []analysis.SuggestedFix{ { Message: suggestedFixMessage(ident.Name), - TextEdits: deleteStmtFromBlock(path, stmt), + TextEdits: edits, }, } } diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/cache/analysis.go similarity index 98% rename from gopls/internal/lsp/cache/analysis.go rename to gopls/internal/cache/analysis.go index dfbb9ef6c0e..0b7bc08d378 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/cache/analysis.go @@ -34,9 +34,9 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/filecache" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/progress" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/progress" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" @@ -150,7 +150,7 @@ import ( // Even if the ultimate consumer decides to ignore errors, // tests and other situations want to be assured of freedom from // errors, not just missing results. This should be recorded. -// - Split this into a subpackage, gopls/internal/lsp/cache/driver, +// - Split this into a subpackage, gopls/internal/cache/driver, // consisting of this file and three helpers from errors.go. // The (*snapshot).Analyze method would stay behind and make calls // to the driver package. @@ -172,7 +172,7 @@ const AnalysisProgressTitle = "Analyzing Dependencies" // The analyzers list must be duplicate free; order does not matter. // // Notifications of progress may be sent to the optional reporter. -func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, analyzers []*settings.Analyzer, reporter *progress.Tracker) ([]*Diagnostic, error) { +func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Package, analyzers []*settings.Analyzer, reporter *progress.Tracker) ([]*Diagnostic, error) { start := time.Now() // for progress reporting var tagStr string // sorted comma-separated list of PackageIDs @@ -1269,10 +1269,21 @@ func (act *action) exec() (interface{}, *actionSummary, error) { factFilter[reflect.TypeOf(f)] = true } + // If the package contains "fixed" files, it's not necessarily an error if we + // can't convert positions. + hasFixedFiles := false + for _, p := range pkg.parsed { + if p.Fixed() { + hasFixedFiles = true + break + } + } + // posToLocation converts from token.Pos to protocol form. // TODO(adonovan): improve error messages. posToLocation := func(start, end token.Pos) (protocol.Location, error) { tokFile := pkg.fset.File(start) + for _, p := range pkg.parsed { if p.Tok == tokFile { if end == token.NoPos { @@ -1281,8 +1292,11 @@ func (act *action) exec() (interface{}, *actionSummary, error) { return p.PosLocation(start, end) } } - return protocol.Location{}, - bug.Errorf("internal error: token.Pos not within package") + errorf := bug.Errorf + if hasFixedFiles { + errorf = fmt.Errorf + } + return protocol.Location{}, errorf("token.Pos not within package") } // Now run the (pkg, analyzer) action. @@ -1299,7 +1313,9 @@ func (act *action) exec() (interface{}, *actionSummary, error) { Report: func(d analysis.Diagnostic) { diagnostic, err := toGobDiagnostic(posToLocation, analyzer, d) if err != nil { - bug.Reportf("internal error converting diagnostic from analyzer %q: %v", analyzer.Name, err) + if !hasFixedFiles { + bug.Reportf("internal error converting diagnostic from analyzer %q: %v", analyzer.Name, err) + } return } diagnostics = append(diagnostics, diagnostic) @@ -1421,7 +1437,7 @@ func requiredAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer { var analyzeSummaryCodec = frob.CodecFor[*analyzeSummary]() -// -- data types for serialization of analysis.Diagnostic and source.Diagnostic -- +// -- data types for serialization of analysis.Diagnostic and golang.Diagnostic -- // (The name says gob but we use frob.) var diagnosticsCodec = frob.CodecFor[[]gobDiagnostic]() diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/cache/cache.go similarity index 51% rename from gopls/internal/lsp/cache/cache.go rename to gopls/internal/cache/cache.go index 72fe36ee302..a6a166aab58 100644 --- a/gopls/internal/lsp/cache/cache.go +++ b/gopls/internal/cache/cache.go @@ -5,15 +5,12 @@ package cache import ( - "context" "reflect" "strconv" "sync/atomic" - "time" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/gocommand" + "golang.org/x/tools/gopls/internal/protocol/command" + "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/memoize" ) @@ -34,39 +31,36 @@ func New(store *memoize.Store) *Cache { id: strconv.FormatInt(index, 10), store: store, memoizedFS: newMemoizedFS(), + modCache: &sharedModCache{ + caches: make(map[string]*imports.DirInfoCache), + timers: make(map[string]*refreshTimer), + }, } return c } -// A Cache holds caching stores that are bundled together for consistency. -// -// TODO(rfindley): once fset and store need not be bundled together, the Cache -// type can be eliminated. +// A Cache holds content that is shared across multiple gopls sessions. type Cache struct { id string + // store holds cached calculations. + // + // TODO(rfindley): at this point, these are not important, as we've moved our + // content-addressable cache to the file system (the filecache package). It + // is unlikely that this shared cache provides any shared value. We should + // consider removing it, replacing current uses with a simpler futures cache, + // as we've done for e.g. type-checked packages. store *memoize.Store - *memoizedFS // implements source.FileSource -} + // memoizedFS holds a shared file.Source that caches reads. + // + // Reads are invalidated when *any* session gets a didChangeWatchedFile + // notification. This is fine: it is the responsibility of memoizedFS to hold + // our best knowledge of the current file system state. + *memoizedFS -// NewSession creates a new gopls session with the given cache and options overrides. -// -// The provided optionsOverrides may be nil. -// -// TODO(rfindley): move this to session.go. -func NewSession(ctx context.Context, c *Cache) *Session { - index := atomic.AddInt64(&sessionIndex, 1) - s := &Session{ - id: strconv.FormatInt(index, 10), - cache: c, - gocmdRunner: &gocommand.Runner{}, - overlayFS: newOverlayFS(c), - parseCache: newParseCache(1 * time.Minute), // keep recently parsed files for a minute, to optimize typing CPU - viewMap: make(map[protocol.DocumentURI]*View), - } - event.Log(ctx, "New session", KeyCreateSession.Of(s)) - return s + // modCache holds the + modCache *sharedModCache } var cacheIndex, sessionIndex, viewIndex int64 @@ -76,6 +70,7 @@ func (c *Cache) MemStats() map[reflect.Type]int { return c.store.Stats() } // FileStats returns information about the set of files stored in the cache. // It is intended for debugging only. -func (c *Cache) FileStats() (files, largest, errs int) { - return c.fileStats() +func (c *Cache) FileStats() (stats command.FileStats) { + stats.Total, stats.Largest, stats.Errs = c.fileStats() + return } diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/cache/check.go similarity index 97% rename from gopls/internal/lsp/cache/check.go rename to gopls/internal/cache/check.go index 502ebb2149c..0af59655ab1 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/cache/check.go @@ -22,11 +22,11 @@ import ( "golang.org/x/mod/module" "golang.org/x/sync/errgroup" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/typerefs" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/filecache" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/cache/typerefs" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/analysisinternal" @@ -610,6 +610,16 @@ func (b *typeCheckBatch) importPackage(ctx context.Context, mp *metadata.Package if item.Path == string(mp.PkgPath) { id = mp.ID pkg = thisPackage + + // debugging issues #60904, #64235 + if pkg.Name() != item.Name { + // This would mean that mp.Name != item.Name, so the + // manifest in the export data of mp.PkgPath is + // inconsistent with mp.Name. Or perhaps there + // are duplicate PkgPath items in the manifest? + return bug.Errorf("internal error: package name is %q, want %q (id=%q, path=%q) (see issue #60904)", + pkg.Name(), item.Name, id, item.Path) + } } else { id = impMap[item.Path] var err error @@ -617,14 +627,22 @@ func (b *typeCheckBatch) importPackage(ctx context.Context, mp *metadata.Package if err != nil { return err } + + // We intentionally duplicate the bug.Errorf calls because + // telemetry tells us only the program counter, not the message. + + // debugging issues #60904, #64235 + if pkg.Name() != item.Name { + // This means that, while reading the manifest of the + // export data of mp.PkgPath, one of its indirect + // dependencies had a name that differs from the + // Metadata.Name + return bug.Errorf("internal error: package name is %q, want %q (id=%q, path=%q) (see issue #60904)", + pkg.Name(), item.Name, id, item.Path) + } } items[i].Pkg = pkg - // debugging issue #60904 - if pkg.Name() != item.Name { - return bug.Errorf("internal error: package name is %q, want %q (id=%q, path=%q) (see issue #60904)", - pkg.Name(), item.Name, id, item.Path) - } } return nil } @@ -724,7 +742,11 @@ func (b *typeCheckBatch) importMap(id PackageID) map[string]PackageID { populateDeps = func(parent *metadata.Package) { for _, id := range parent.DepsByPkgPath { mp := b.handles[id].mp - if _, ok := impMap[string(mp.PkgPath)]; ok { + if prevID, ok := impMap[string(mp.PkgPath)]; ok { + // debugging #63822 + if prevID != mp.ID { + bug.Reportf("inconsistent view of dependencies") + } continue } impMap[string(mp.PkgPath)] = mp.ID @@ -1565,8 +1587,8 @@ func (b *typeCheckBatch) checkPackage(ctx context.Context, ph *packageHandle) (* return &Package{ph.mp, ph.loadDiagnostics, pkg}, nil } -// TODO(golang/go#63472): this looks wrong with the new Go version syntax. -var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) +// e.g. "go1" or "go1.2" or "go1.2.3" +var goVersionRx = regexp.MustCompile(`^go[1-9][0-9]*(?:\.(0|[1-9][0-9]*)){0,2}$`) func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs, onError func(e error)) *types.Config { cfg := &types.Config{ @@ -1693,7 +1715,7 @@ func depsErrors(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) ( Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err), SuggestedFixes: goGetQuickFixes(mp.Module != nil, imp.cgf.URI, item), } - if !BundleQuickFixes(diag) { + if !bundleQuickFixes(diag) { bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message) } errors = append(errors, diag) @@ -1736,7 +1758,7 @@ func depsErrors(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) ( Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err), SuggestedFixes: goGetQuickFixes(true, pm.URI, item), } - if !BundleQuickFixes(diag) { + if !bundleQuickFixes(diag) { bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message) } errors = append(errors, diag) diff --git a/gopls/internal/lsp/cache/constraints.go b/gopls/internal/cache/constraints.go similarity index 100% rename from gopls/internal/lsp/cache/constraints.go rename to gopls/internal/cache/constraints.go diff --git a/gopls/internal/lsp/cache/constraints_test.go b/gopls/internal/cache/constraints_test.go similarity index 74% rename from gopls/internal/lsp/cache/constraints_test.go rename to gopls/internal/cache/constraints_test.go index 9adf01e6cea..23c9f39cb19 100644 --- a/gopls/internal/lsp/cache/constraints_test.go +++ b/gopls/internal/cache/constraints_test.go @@ -94,3 +94,33 @@ func TestIsStandaloneFile(t *testing.T) { }) } } + +func TestVersionRegexp(t *testing.T) { + // good + for _, s := range []string{ + "go1", + "go1.2", + "go1.2.3", + "go1.0.33", + } { + if !goVersionRx.MatchString(s) { + t.Errorf("Valid Go version %q does not match the regexp", s) + } + } + + // bad + for _, s := range []string{ + "go", // missing numbers + "go0", // Go starts at 1 + "go01", // leading zero + "go1.π", // non-decimal + "go1.-1", // negative + "go1.02.3", // leading zero + "go1.2.3.4", // too many segments + "go1.2.3-pre", // textual suffix + } { + if goVersionRx.MatchString(s) { + t.Errorf("Invalid Go version %q unexpectedly matches the regexp", s) + } + } +} diff --git a/gopls/internal/lsp/cache/debug.go b/gopls/internal/cache/debug.go similarity index 100% rename from gopls/internal/lsp/cache/debug.go rename to gopls/internal/cache/debug.go diff --git a/gopls/internal/lsp/cache/diagnostics.go b/gopls/internal/cache/diagnostics.go similarity index 92% rename from gopls/internal/lsp/cache/diagnostics.go rename to gopls/internal/cache/diagnostics.go index 76c82630cc6..5489b5645b6 100644 --- a/gopls/internal/lsp/cache/diagnostics.go +++ b/gopls/internal/cache/diagnostics.go @@ -8,7 +8,7 @@ import ( "encoding/json" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" ) @@ -37,7 +37,7 @@ type Diagnostic struct { URI protocol.DocumentURI // of diagnosed file (not diagnostic documentation) Range protocol.Range Severity protocol.DiagnosticSeverity - Code string + Code string // analysis.Diagnostic.Category (or "default" if empty) or hidden go/types error code CodeHref string // Source is a human-readable description of the source of the error. @@ -119,10 +119,10 @@ type quickFixesJSON struct { Fixes []protocol.CodeAction } -// BundleQuickFixes attempts to bundle sd.SuggestedFixes into the +// bundleQuickFixes attempts to bundle sd.SuggestedFixes into the // sd.BundledFixes field, so that it can be round-tripped through the client. // It returns false if the quick-fixes cannot be bundled. -func BundleQuickFixes(sd *Diagnostic) bool { +func bundleQuickFixes(sd *Diagnostic) bool { if len(sd.SuggestedFixes) == 0 { return true } @@ -164,16 +164,13 @@ func BundleQuickFixes(sd *Diagnostic) bool { // BundledQuickFixes extracts any bundled codeActions from the // diag.Data field. func BundledQuickFixes(diag protocol.Diagnostic) []protocol.CodeAction { - // Clients may express "no fixes" in a variety of ways (#64503). - if diag.Data == nil || - len(*diag.Data) == 0 || - len(*diag.Data) == 4 && string(*diag.Data) == "null" { - return nil - } var fix quickFixesJSON - if err := json.Unmarshal(*diag.Data, &fix); err != nil { - bug.Reportf("unmarshalling quick fix: %v", err) - return nil + if diag.Data != nil { + err := protocol.UnmarshalJSON(*diag.Data, &fix) + if err != nil { + bug.Reportf("unmarshalling quick fix: %v", err) + return nil + } } var actions []protocol.CodeAction diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/cache/errors.go similarity index 86% rename from gopls/internal/lsp/cache/errors.go rename to gopls/internal/cache/errors.go index ded226e4a07..83382b0aad2 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/cache/errors.go @@ -6,7 +6,7 @@ package cache // This file defines routines to convert diagnostics from go list, go // get, go/packages, parsing, type checking, and analysis into -// source.Diagnostic form, and suggesting quick fixes. +// golang.Diagnostic form, and suggesting quick fixes. import ( "context" @@ -21,11 +21,10 @@ import ( "strings" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/analysis/embeddirective" + "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/typesinternal" @@ -295,27 +294,47 @@ func toSourceDiagnostic(srcAnalyzer *settings.Analyzer, gobDiag *gobDiagnostic) Related: related, Tags: srcAnalyzer.Tag, } - if canFix(srcAnalyzer, diag) { - // We cross the set of fixes (whether edit- or command-based) - // with the set of kinds, as a single fix may represent more - // than one kind of action (e.g. refactor, quickfix, fixall), - // each corresponding to a distinct client UI element - // or operation. - kinds := srcAnalyzer.ActionKind - if len(kinds) == 0 { - kinds = []protocol.CodeActionKind{protocol.QuickFix} - } - // Accumulate edit-based fixes supplied by the diagnostic itself. - fixes := suggestedAnalysisFixes(gobDiag, kinds) + // We cross the set of fixes (whether edit- or command-based) + // with the set of kinds, as a single fix may represent more + // than one kind of action (e.g. refactor, quickfix, fixall), + // each corresponding to a distinct client UI element + // or operation. + kinds := srcAnalyzer.ActionKinds + if len(kinds) == 0 { + kinds = []protocol.CodeActionKind{protocol.QuickFix} + } - // Accumulate command-based fixes computed on demand by - // (logic adjacent to) the analyzer. - if srcAnalyzer.Fix != "" { - cmd, err := command.NewApplyFixCommand(gobDiag.Message, command.ApplyFixArgs{ + var fixes []SuggestedFix + for _, fix := range gobDiag.SuggestedFixes { + if len(fix.TextEdits) > 0 { + // Accumulate edit-based fixes supplied by the diagnostic itself. + edits := make(map[protocol.DocumentURI][]protocol.TextEdit) + for _, e := range fix.TextEdits { + uri := e.Location.URI + edits[uri] = append(edits[uri], protocol.TextEdit{ + Range: e.Location.Range, + NewText: string(e.NewText), + }) + } + for _, kind := range kinds { + fixes = append(fixes, SuggestedFix{ + Title: fix.Message, + Edits: edits, + ActionKind: kind, + }) + } + + } else { + // Accumulate command-based fixes, whose edits + // are not provided by the analyzer but are computed on demand + // by logic "adjacent to" the analyzer. + // + // The analysis.Diagnostic.Category is used as the fix name. + cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ + Fix: diag.Code, URI: gobDiag.Location.URI, Range: gobDiag.Location.Range, - Fix: string(srcAnalyzer.Fix), }) if err != nil { // JSON marshalling of these argument values cannot fail. @@ -324,9 +343,16 @@ func toSourceDiagnostic(srcAnalyzer *settings.Analyzer, gobDiag *gobDiagnostic) for _, kind := range kinds { fixes = append(fixes, SuggestedFixFromCommand(cmd, kind)) } + + // Ensure that the analyzer specifies a category for all its no-edit fixes. + // This is asserted by analysistest.RunWithSuggestedFixes, but there + // may be gaps in test coverage. + if diag.Code == "" || diag.Code == "default" { + bug.Reportf("missing Diagnostic.Code: %#v", *diag) + } } - diag.SuggestedFixes = fixes } + diag.SuggestedFixes = fixes // If the fixes only delete code, assume that the diagnostic is reporting dead code. if onlyDeletions(diag.SuggestedFixes) { @@ -335,17 +361,6 @@ func toSourceDiagnostic(srcAnalyzer *settings.Analyzer, gobDiag *gobDiagnostic) return diag } -// canFix reports whether the Analyzer can fix the Diagnostic. -func canFix(a *settings.Analyzer, diag *Diagnostic) bool { - if a.Fix == settings.AddEmbedImport { - return diag.Message == embeddirective.MissingImportMessage - } - - // This doesn't make sense, but preserves pre-existing semantics. - // TODO(rfindley): reconcile the semantics of Fix and suggestedAnalysisFixes. - return true -} - // onlyDeletions returns true if fixes is non-empty and all of the suggested // fixes are deletions. func onlyDeletions(fixes []SuggestedFix) bool { @@ -380,31 +395,6 @@ func BuildLink(target, path, anchor string) string { return link + "#" + anchor } -// suggestedAnalysisFixes converts edit-based fixes associated -// with a gobDiagnostic to cache.SuggestedFixes. -// It returns the cross product of fixes and kinds. -func suggestedAnalysisFixes(diag *gobDiagnostic, kinds []protocol.CodeActionKind) []SuggestedFix { - var fixes []SuggestedFix - for _, fix := range diag.SuggestedFixes { - edits := make(map[protocol.DocumentURI][]protocol.TextEdit) - for _, e := range fix.TextEdits { - uri := e.Location.URI - edits[uri] = append(edits[uri], protocol.TextEdit{ - Range: e.Location.Range, - NewText: string(e.NewText), - }) - } - for _, kind := range kinds { - fixes = append(fixes, SuggestedFix{ - Title: fix.Message, - Edits: edits, - ActionKind: kind, - }) - } - } - return fixes -} - func parseGoListError(e packages.Error, dir string) (filename string, line, col8 int) { input := e.Pos if input == "" { diff --git a/gopls/internal/lsp/cache/errors_test.go b/gopls/internal/cache/errors_test.go similarity index 98% rename from gopls/internal/lsp/cache/errors_test.go rename to gopls/internal/cache/errors_test.go index 38bd652c649..56b29c3c55b 100644 --- a/gopls/internal/lsp/cache/errors_test.go +++ b/gopls/internal/cache/errors_test.go @@ -11,7 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" ) func TestParseErrorMessage(t *testing.T) { diff --git a/gopls/internal/lsp/cache/filemap.go b/gopls/internal/cache/filemap.go similarity index 71% rename from gopls/internal/lsp/cache/filemap.go rename to gopls/internal/cache/filemap.go index 8ca7ab79721..ee64d7c32c3 100644 --- a/gopls/internal/lsp/cache/filemap.go +++ b/gopls/internal/cache/filemap.go @@ -8,7 +8,7 @@ import ( "path/filepath" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/persistent" ) @@ -17,21 +17,21 @@ import ( // file. type fileMap struct { files *persistent.Map[protocol.DocumentURI, file.Handle] - overlays *persistent.Map[protocol.DocumentURI, *Overlay] // the subset of files that are overlays + overlays *persistent.Map[protocol.DocumentURI, *overlay] // the subset of files that are overlays dirs *persistent.Set[string] // all dirs containing files; if nil, dirs have not been initialized } func newFileMap() *fileMap { return &fileMap{ files: new(persistent.Map[protocol.DocumentURI, file.Handle]), - overlays: new(persistent.Map[protocol.DocumentURI, *Overlay]), + overlays: new(persistent.Map[protocol.DocumentURI, *overlay]), dirs: new(persistent.Set[string]), } } -// Clone creates a copy of the fileMap, incorporating the changes specified by +// clone creates a copy of the fileMap, incorporating the changes specified by // the changes map. -func (m *fileMap) Clone(changes map[protocol.DocumentURI]file.Handle) *fileMap { +func (m *fileMap) clone(changes map[protocol.DocumentURI]file.Handle) *fileMap { m2 := &fileMap{ files: m.files.Clone(), overlays: m.overlays.Clone(), @@ -52,18 +52,18 @@ func (m *fileMap) Clone(changes map[protocol.DocumentURI]file.Handle) *fileMap { // first, as a set before a deletion would result in pointless work. for uri, fh := range changes { if !fileExists(fh) { - m2.Delete(uri) + m2.delete(uri) } } for uri, fh := range changes { if fileExists(fh) { - m2.Set(uri, fh) + m2.set(uri, fh) } } return m2 } -func (m *fileMap) Destroy() { +func (m *fileMap) destroy() { m.files.Destroy() m.overlays.Destroy() if m.dirs != nil { @@ -71,24 +71,24 @@ func (m *fileMap) Destroy() { } } -// Get returns the file handle mapped by the given key, or (nil, false) if the +// get returns the file handle mapped by the given key, or (nil, false) if the // key is not present. -func (m *fileMap) Get(key protocol.DocumentURI) (file.Handle, bool) { +func (m *fileMap) get(key protocol.DocumentURI) (file.Handle, bool) { return m.files.Get(key) } -// Range calls f for each (uri, fh) in the map. -func (m *fileMap) Range(f func(uri protocol.DocumentURI, fh file.Handle)) { +// foreach calls f for each (uri, fh) in the map. +func (m *fileMap) foreach(f func(uri protocol.DocumentURI, fh file.Handle)) { m.files.Range(f) } -// Set stores the given file handle for key, updating overlays and directories +// set stores the given file handle for key, updating overlays and directories // accordingly. -func (m *fileMap) Set(key protocol.DocumentURI, fh file.Handle) { +func (m *fileMap) set(key protocol.DocumentURI, fh file.Handle) { m.files.Set(key, fh, nil) // update overlays - if o, ok := fh.(*Overlay); ok { + if o, ok := fh.(*overlay); ok { m.overlays.Set(key, o, nil) } else { // Setting a non-overlay must delete the corresponding overlay, to preserve @@ -111,9 +111,9 @@ func (m *fileMap) addDirs(u protocol.DocumentURI) { } } -// Delete removes a file from the map, and updates overlays and dirs +// delete removes a file from the map, and updates overlays and dirs // accordingly. -func (m *fileMap) Delete(key protocol.DocumentURI) { +func (m *fileMap) delete(key protocol.DocumentURI) { m.files.Delete(key) m.overlays.Delete(key) @@ -127,20 +127,20 @@ func (m *fileMap) Delete(key protocol.DocumentURI) { } } -// Overlays returns a new unordered array of overlay files. -func (m *fileMap) Overlays() []*Overlay { - var overlays []*Overlay - m.overlays.Range(func(_ protocol.DocumentURI, o *Overlay) { +// getOverlays returns a new unordered array of overlay files. +func (m *fileMap) getOverlays() []*overlay { + var overlays []*overlay + m.overlays.Range(func(_ protocol.DocumentURI, o *overlay) { overlays = append(overlays, o) }) return overlays } -// Dirs reports returns the set of dirs observed by the fileMap. +// getDirs reports returns the set of dirs observed by the fileMap. // // This operation mutates the fileMap. // The result must not be mutated by the caller. -func (m *fileMap) Dirs() *persistent.Set[string] { +func (m *fileMap) getDirs() *persistent.Set[string] { if m.dirs == nil { m.dirs = new(persistent.Set[string]) m.files.Range(func(u protocol.DocumentURI, _ file.Handle) { diff --git a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/cache/filemap_test.go similarity index 89% rename from gopls/internal/lsp/cache/filemap_test.go rename to gopls/internal/cache/filemap_test.go index d829d243276..13f2c1a9ccd 100644 --- a/gopls/internal/lsp/cache/filemap_test.go +++ b/gopls/internal/cache/filemap_test.go @@ -11,7 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" ) func TestFileMap(t *testing.T) { @@ -72,18 +72,18 @@ func TestFileMap(t *testing.T) { case set: var fh file.Handle if op.overlay { - fh = &Overlay{uri: uri} + fh = &overlay{uri: uri} } else { - fh = &DiskFile{uri: uri} + fh = &diskFile{uri: uri} } - m.Set(uri, fh) + m.set(uri, fh) case del: - m.Delete(uri) + m.delete(uri) } } var gotFiles []string - m.Range(func(uri protocol.DocumentURI, _ file.Handle) { + m.foreach(func(uri protocol.DocumentURI, _ file.Handle) { gotFiles = append(gotFiles, normalize(uri.Path())) }) sort.Strings(gotFiles) @@ -92,7 +92,7 @@ func TestFileMap(t *testing.T) { } var gotOverlays []string - for _, o := range m.Overlays() { + for _, o := range m.getOverlays() { gotOverlays = append(gotOverlays, normalize(o.URI().Path())) } if diff := cmp.Diff(test.wantOverlays, gotOverlays); diff != "" { @@ -100,7 +100,7 @@ func TestFileMap(t *testing.T) { } var gotDirs []string - m.Dirs().Range(func(dir string) { + m.getDirs().Range(func(dir string) { gotDirs = append(gotDirs, normalize(dir)) }) sort.Strings(gotDirs) diff --git a/gopls/internal/lsp/cache/filterer.go b/gopls/internal/cache/filterer.go similarity index 100% rename from gopls/internal/lsp/cache/filterer.go rename to gopls/internal/cache/filterer.go diff --git a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/cache/fs_memoized.go similarity index 84% rename from gopls/internal/lsp/cache/fs_memoized.go rename to gopls/internal/cache/fs_memoized.go index 11f877dce9c..dd8293fad75 100644 --- a/gopls/internal/lsp/cache/fs_memoized.go +++ b/gopls/internal/cache/fs_memoized.go @@ -11,7 +11,7 @@ import ( "time" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/robustio" @@ -24,16 +24,16 @@ type memoizedFS struct { // filesByID maps existing file inodes to the result of a read. // (The read may have failed, e.g. due to EACCES or a delete between stat+read.) // Each slice is a non-empty list of aliases: different URIs. - filesByID map[robustio.FileID][]*DiskFile + filesByID map[robustio.FileID][]*diskFile } func newMemoizedFS() *memoizedFS { - return &memoizedFS{filesByID: make(map[robustio.FileID][]*DiskFile)} + return &memoizedFS{filesByID: make(map[robustio.FileID][]*diskFile)} } -// A DiskFile is a file on the filesystem, or a failure to read one. -// It implements the source.FileHandle interface. -type DiskFile struct { +// A diskFile is a file in the filesystem, or a failure to read one. +// It implements the file.Source interface. +type diskFile struct { uri protocol.DocumentURI modTime time.Time content []byte @@ -41,25 +41,25 @@ type DiskFile struct { err error } -func (h *DiskFile) URI() protocol.DocumentURI { return h.uri } +func (h *diskFile) URI() protocol.DocumentURI { return h.uri } -func (h *DiskFile) Identity() file.Identity { +func (h *diskFile) Identity() file.Identity { return file.Identity{ URI: h.uri, Hash: h.hash, } } -func (h *DiskFile) SameContentsOnDisk() bool { return true } -func (h *DiskFile) Version() int32 { return 0 } -func (h *DiskFile) Content() ([]byte, error) { return h.content, h.err } +func (h *diskFile) SameContentsOnDisk() bool { return true } +func (h *diskFile) Version() int32 { return 0 } +func (h *diskFile) Content() ([]byte, error) { return h.content, h.err } // ReadFile stats and (maybe) reads the file, updates the cache, and returns it. func (fs *memoizedFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) { id, mtime, err := robustio.GetFileID(uri.Path()) if err != nil { // file does not exist - return &DiskFile{ + return &diskFile{ err: err, uri: uri, }, nil @@ -79,7 +79,7 @@ func (fs *memoizedFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (f fs.mu.Lock() fhs, ok := fs.filesByID[id] if ok && fhs[0].modTime.Equal(mtime) { - var fh *DiskFile + var fh *diskFile // We have already seen this file and it has not changed. for _, h := range fhs { if h.uri == uri { @@ -108,7 +108,7 @@ func (fs *memoizedFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (f fs.mu.Lock() if !recentlyModified { - fs.filesByID[id] = []*DiskFile{fh} + fs.filesByID[id] = []*diskFile{fh} } else { delete(fs.filesByID, id) } @@ -141,7 +141,7 @@ func (fs *memoizedFS) fileStats() (files, largest, errs int) { // ioLimit limits the number of parallel file reads per process. var ioLimit = make(chan struct{}, 128) -func readFile(ctx context.Context, uri protocol.DocumentURI, mtime time.Time) (*DiskFile, error) { +func readFile(ctx context.Context, uri protocol.DocumentURI, mtime time.Time) (*diskFile, error) { select { case ioLimit <- struct{}{}: case <-ctx.Done(): @@ -161,7 +161,7 @@ func readFile(ctx context.Context, uri protocol.DocumentURI, mtime time.Time) (* if err != nil { content = nil // just in case } - return &DiskFile{ + return &diskFile{ modTime: mtime, uri: uri, content: content, diff --git a/gopls/internal/lsp/cache/fs_overlay.go b/gopls/internal/cache/fs_overlay.go similarity index 61% rename from gopls/internal/lsp/cache/fs_overlay.go rename to gopls/internal/cache/fs_overlay.go index 8abfbc969ad..265598bb967 100644 --- a/gopls/internal/lsp/cache/fs_overlay.go +++ b/gopls/internal/cache/fs_overlay.go @@ -9,7 +9,7 @@ import ( "sync" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" ) // An overlayFS is a file.Source that keeps track of overlays on top of a @@ -18,21 +18,21 @@ type overlayFS struct { delegate file.Source mu sync.Mutex - overlays map[protocol.DocumentURI]*Overlay + overlays map[protocol.DocumentURI]*overlay } func newOverlayFS(delegate file.Source) *overlayFS { return &overlayFS{ delegate: delegate, - overlays: make(map[protocol.DocumentURI]*Overlay), + overlays: make(map[protocol.DocumentURI]*overlay), } } // Overlays returns a new unordered array of overlays. -func (fs *overlayFS) Overlays() []*Overlay { +func (fs *overlayFS) Overlays() []*overlay { fs.mu.Lock() defer fs.mu.Unlock() - overlays := make([]*Overlay, 0, len(fs.overlays)) + overlays := make([]*overlay, 0, len(fs.overlays)) for _, overlay := range fs.overlays { overlays = append(overlays, overlay) } @@ -49,9 +49,10 @@ func (fs *overlayFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (fi return fs.delegate.ReadFile(ctx, uri) } -// An Overlay is a file open in the editor. It may have unsaved edits. -// It implements the file.Handle interface. -type Overlay struct { +// An overlay is a file open in the editor. It may have unsaved edits. +// It implements the file.Handle interface, and the implicit contract +// of the debug.FileTmpl template. +type overlay struct { uri protocol.DocumentURI content []byte hash file.Hash @@ -63,16 +64,16 @@ type Overlay struct { saved bool } -func (o *Overlay) URI() protocol.DocumentURI { return o.uri } +func (o *overlay) URI() protocol.DocumentURI { return o.uri } -func (o *Overlay) Identity() file.Identity { +func (o *overlay) Identity() file.Identity { return file.Identity{ URI: o.uri, Hash: o.hash, } } -func (o *Overlay) Content() ([]byte, error) { return o.content, nil } -func (o *Overlay) Version() int32 { return o.version } -func (o *Overlay) SameContentsOnDisk() bool { return o.saved } -func (o *Overlay) Kind() file.Kind { return o.kind } +func (o *overlay) Content() ([]byte, error) { return o.content, nil } +func (o *overlay) Version() int32 { return o.version } +func (o *overlay) SameContentsOnDisk() bool { return o.saved } +func (o *overlay) Kind() file.Kind { return o.kind } diff --git a/gopls/internal/cache/imports.go b/gopls/internal/cache/imports.go new file mode 100644 index 00000000000..7964427e528 --- /dev/null +++ b/gopls/internal/cache/imports.go @@ -0,0 +1,229 @@ +// 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 cache + +import ( + "context" + "fmt" + "sync" + "time" + + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/keys" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/imports" +) + +// refreshTimer implements delayed asynchronous refreshing of state. +// +// See the [refreshTimer.schedule] documentation for more details. +type refreshTimer struct { + mu sync.Mutex + duration time.Duration + timer *time.Timer + refreshFn func() +} + +// newRefreshTimer constructs a new refresh timer which schedules refreshes +// using the given function. +func newRefreshTimer(refresh func()) *refreshTimer { + return &refreshTimer{ + refreshFn: refresh, + } +} + +// schedule schedules the refresh function to run at some point in the future, +// if no existing refresh is already scheduled. +// +// At a minimum, scheduled refreshes are delayed by 30s, but they may be +// delayed longer to keep their expected execution time under 2% of wall clock +// time. +func (t *refreshTimer) schedule() { + t.mu.Lock() + defer t.mu.Unlock() + + if t.timer == nil { + // Don't refresh more than twice per minute. + delay := 30 * time.Second + // Don't spend more than ~2% of the time refreshing. + if adaptive := 50 * t.duration; adaptive > delay { + delay = adaptive + } + t.timer = time.AfterFunc(delay, func() { + start := time.Now() + t.refreshFn() + t.mu.Lock() + t.duration = time.Since(start) + t.timer = nil + t.mu.Unlock() + }) + } +} + +// A sharedModCache tracks goimports state for GOMODCACHE directories +// (each session may have its own GOMODCACHE). +// +// This state is refreshed independently of view-specific imports state. +type sharedModCache struct { + mu sync.Mutex + caches map[string]*imports.DirInfoCache // GOMODCACHE -> cache content; never invalidated + timers map[string]*refreshTimer // GOMODCACHE -> timer +} + +func (c *sharedModCache) dirCache(dir string) *imports.DirInfoCache { + c.mu.Lock() + defer c.mu.Unlock() + + cache, ok := c.caches[dir] + if !ok { + cache = imports.NewDirInfoCache() + c.caches[dir] = cache + } + return cache +} + +// refreshDir schedules a refresh of the given directory, which must be a +// module cache. +func (c *sharedModCache) refreshDir(ctx context.Context, dir string, logf func(string, ...any)) { + cache := c.dirCache(dir) + + c.mu.Lock() + defer c.mu.Unlock() + timer, ok := c.timers[dir] + if !ok { + timer = newRefreshTimer(func() { + _, done := event.Start(ctx, "cache.sharedModCache.refreshDir", tag.Directory.Of(dir)) + defer done() + imports.ScanModuleCache(dir, cache, logf) + }) + c.timers[dir] = timer + } + + timer.schedule() +} + +// importsState tracks view-specific imports state. +type importsState struct { + ctx context.Context + modCache *sharedModCache + refreshTimer *refreshTimer + + mu sync.Mutex + processEnv *imports.ProcessEnv + cachedModFileHash file.Hash +} + +// newImportsState constructs a new imports state for running goimports +// functions via [runProcessEnvFunc]. +// +// The returned state will automatically refresh itself following a call to +// runProcessEnvFunc. +func newImportsState(backgroundCtx context.Context, modCache *sharedModCache, env *imports.ProcessEnv) *importsState { + s := &importsState{ + ctx: backgroundCtx, + modCache: modCache, + processEnv: env, + } + s.refreshTimer = newRefreshTimer(s.refreshProcessEnv) + return s +} + +// runProcessEnvFunc runs goimports. +// +// Any call to runProcessEnvFunc will schedule a refresh of the imports state +// at some point in the future, if such a refresh is not already scheduled. See +// [refreshTimer] for more details. +func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *Snapshot, fn func(context.Context, *imports.Options) error) error { + ctx, done := event.Start(ctx, "cache.importsState.runProcessEnvFunc") + defer done() + + s.mu.Lock() + defer s.mu.Unlock() + + // Find the hash of active mod files, if any. Using the unsaved content + // is slightly wasteful, since we'll drop caches a little too often, but + // the mod file shouldn't be changing while people are autocompleting. + // + // TODO(rfindley): consider instead hashing on-disk modfiles here. + var modFileHash file.Hash + for m := range snapshot.view.workspaceModFiles { + fh, err := snapshot.ReadFile(ctx, m) + if err != nil { + return err + } + modFileHash.XORWith(fh.Identity().Hash) + } + + // If anything relevant to imports has changed, clear caches and + // update the processEnv. Clearing caches blocks on any background + // scans. + if modFileHash != s.cachedModFileHash { + s.processEnv.ClearModuleInfo() + s.cachedModFileHash = modFileHash + } + + // Run the user function. + opts := &imports.Options{ + // Defaults. + AllErrors: true, + Comments: true, + Fragment: true, + FormatOnly: false, + TabIndent: true, + TabWidth: 8, + Env: s.processEnv, + LocalPrefix: snapshot.Options().Local, + } + + if err := fn(ctx, opts); err != nil { + return err + } + + // Refresh the imports resolver after usage. This may seem counterintuitive, + // since it means the first ProcessEnvFunc after a long period of inactivity + // may be stale, but in practice we run ProcessEnvFuncs frequently during + // active development (e.g. during completion), and so this mechanism will be + // active while gopls is in use, and inactive when gopls is idle. + s.refreshTimer.schedule() + + // TODO(rfindley): the GOMODCACHE value used here isn't directly tied to the + // ProcessEnv.Env["GOMODCACHE"], though they should theoretically always + // agree. It would be better if we guaranteed this, possibly by setting all + // required environment variables in ProcessEnv.Env, to avoid the redundant + // Go command invocation. + gomodcache := snapshot.view.folder.Env.GOMODCACHE + s.modCache.refreshDir(s.ctx, gomodcache, s.processEnv.Logf) + + return nil +} + +func (s *importsState) refreshProcessEnv() { + ctx, done := event.Start(s.ctx, "cache.importsState.refreshProcessEnv") + defer done() + + start := time.Now() + + s.mu.Lock() + resolver, err := s.processEnv.GetResolver() + s.mu.Unlock() + if err != nil { + return + } + + event.Log(s.ctx, "background imports cache refresh starting") + + // Prime the new resolver before updating the processEnv, so that gopls + // doesn't wait on an unprimed cache. + if err := imports.PrimeCache(context.Background(), resolver); err == nil { + event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start))) + } else { + event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err)) + } + + s.mu.Lock() + s.processEnv.UpdateResolver(resolver) + s.mu.Unlock() +} diff --git a/gopls/internal/lsp/cache/keys.go b/gopls/internal/cache/keys.go similarity index 98% rename from gopls/internal/lsp/cache/keys.go rename to gopls/internal/cache/keys.go index 449daba3a9e..664e539edbc 100644 --- a/gopls/internal/lsp/cache/keys.go +++ b/gopls/internal/cache/keys.go @@ -4,6 +4,8 @@ package cache +// session event tracing + import ( "io" diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/cache/load.go similarity index 90% rename from gopls/internal/lsp/cache/load.go rename to gopls/internal/cache/load.go index 9831a4d2512..4bbeb2d160a 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/cache/load.go @@ -16,9 +16,9 @@ import ( "time" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/immutable" "golang.org/x/tools/gopls/internal/util/pathutil" @@ -151,6 +151,45 @@ func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc event.Log(ctx, eventName, labels...) } + if standalone { + // Handle standalone package result. + // + // In general, this should just be a single "command-line-arguments" + // package containing the requested file. However, if the file is a test + // file, go/packages may return test variants of the command-line-arguments + // package. We don't support this; theoretically we could, but it seems + // unnecessarily complicated. + // + // Prior to golang/go#64233 we just assumed that we'd get exactly one + // package here. The categorization of bug reports below may be a bit + // verbose, but anticipates that perhaps we don't fully understand + // possible failure modes. + errorf := bug.Errorf + if s.view.typ == GoPackagesDriverView { + errorf = fmt.Errorf // all bets are off + } + + var standalonePkg *packages.Package + for _, pkg := range pkgs { + if pkg.ID == "command-line-arguments" { + if standalonePkg != nil { + return errorf("internal error: go/packages returned multiple standalone packages") + } + standalonePkg = pkg + } else if packagesinternal.GetForTest(pkg) == "" && !strings.HasSuffix(pkg.ID, ".test") { + return errorf("internal error: go/packages returned unexpected package %q for standalone file", pkg.ID) + } + } + if standalonePkg == nil { + return errorf("internal error: go/packages failed to return non-test standalone package") + } + if len(standalonePkg.CompiledGoFiles) > 0 { + pkgs = []*packages.Package{standalonePkg} + } else { + pkgs = nil + } + } + if len(pkgs) == 0 { if err == nil { err = errNoPackages @@ -158,10 +197,6 @@ func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc return fmt.Errorf("packages.Load error: %w", err) } - if standalone && len(pkgs) > 1 { - return bug.Errorf("internal error: go/packages returned multiple packages for standalone file") - } - moduleErrs := make(map[string][]packages.Error) // module path -> errors filterFunc := s.view.filterFunc() newMetadata := make(map[PackageID]*metadata.Package) @@ -306,13 +341,26 @@ func buildMetadata(updates map[PackageID]*metadata.Package, pkg *packages.Packag id := PackageID(pkg.ID) if metadata.IsCommandLineArguments(id) { - if len(pkg.CompiledGoFiles) != 1 { - bug.Reportf("unexpected files in command-line-arguments package: %v", pkg.CompiledGoFiles) + var f string // file to use as disambiguating suffix + if len(pkg.CompiledGoFiles) > 0 { + f = pkg.CompiledGoFiles[0] + + // If there are multiple files, + // we can't use only the first. + // (Can this happen? #64557) + if len(pkg.CompiledGoFiles) > 1 { + bug.Reportf("unexpected files in command-line-arguments package: %v", pkg.CompiledGoFiles) + return + } + } else if len(pkg.IgnoredFiles) > 0 { + // A file=empty.go query results in IgnoredFiles=[empty.go]. + f = pkg.IgnoredFiles[0] + } else { + bug.Reportf("command-line-arguments package has neither CompiledGoFiles nor IgnoredFiles: %#v", "") //*pkg.Metadata) return } - suffix := pkg.CompiledGoFiles[0] - id = PackageID(pkg.ID + suffix) - pkgPath = PackagePath(pkg.PkgPath + suffix) + id = PackageID(pkg.ID + f) + pkgPath = PackagePath(pkg.PkgPath + f) } // Duplicate? @@ -601,8 +649,8 @@ func containsOpenFileLocked(s *Snapshot, mp *metadata.Package) bool { } for uri := range uris { - fh, _ := s.files.Get(uri) - if _, open := fh.(*Overlay); open { + fh, _ := s.files.get(uri) + if _, open := fh.(*overlay); open { return true } } diff --git a/gopls/internal/lsp/cache/metadata/cycle_test.go b/gopls/internal/cache/metadata/cycle_test.go similarity index 80% rename from gopls/internal/lsp/cache/metadata/cycle_test.go rename to gopls/internal/cache/metadata/cycle_test.go index 3597be73b84..09628d881e9 100644 --- a/gopls/internal/lsp/cache/metadata/cycle_test.go +++ b/gopls/internal/cache/metadata/cycle_test.go @@ -8,58 +8,25 @@ import ( "sort" "strings" "testing" + + "golang.org/x/tools/gopls/internal/util/bug" ) +func init() { + bug.PanicOnBugs = true +} + // This is an internal test of the breakImportCycles logic. func TestBreakImportCycles(t *testing.T) { - type Graph = map[PackageID]*Package - - // cyclic returns a description of a cycle, - // if the graph is cyclic, otherwise "". - cyclic := func(graph Graph) string { - const ( - unvisited = 0 - visited = 1 - onstack = 2 - ) - color := make(map[PackageID]int) - var visit func(id PackageID) string - visit = func(id PackageID) string { - switch color[id] { - case unvisited: - color[id] = onstack - case onstack: - return string(id) // cycle! - case visited: - return "" - } - if mp := graph[id]; mp != nil { - for _, depID := range mp.DepsByPkgPath { - if cycle := visit(depID); cycle != "" { - return string(id) + "->" + cycle - } - } - } - color[id] = visited - return "" - } - for id := range graph { - if cycle := visit(id); cycle != "" { - return cycle - } - } - return "" - } - // parse parses an import dependency graph. // The input is a semicolon-separated list of node descriptions. // Each node description is a package ID, optionally followed by // "->" and a comma-separated list of successor IDs. // Thus "a->b;b->c,d;e" represents the set of nodes {a,b,e} // and the set of edges {a->b, b->c, b->d}. - parse := func(s string) Graph { - m := make(Graph) + parse := func(s string) map[PackageID]*Package { + m := make(map[PackageID]*Package) makeNode := func(name string) *Package { id := PackageID(name) n, ok := m[id] @@ -98,7 +65,7 @@ func TestBreakImportCycles(t *testing.T) { // format formats an import graph, in lexicographic order, // in the notation of parse, but with a "!" after the name // of each node that has errors. - format := func(graph Graph) string { + format := func(graph map[PackageID]*Package) string { var items []string for _, mp := range graph { item := string(mp.ID) diff --git a/gopls/internal/lsp/cache/metadata/graph.go b/gopls/internal/cache/metadata/graph.go similarity index 88% rename from gopls/internal/lsp/cache/metadata/graph.go rename to gopls/internal/cache/metadata/graph.go index ca8c68343fe..f09822d3575 100644 --- a/gopls/internal/lsp/cache/metadata/graph.go +++ b/gopls/internal/cache/metadata/graph.go @@ -8,7 +8,7 @@ import ( "sort" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" ) @@ -38,6 +38,22 @@ func (g *Graph) Update(updates map[PackageID]*Package) *Graph { return g } + // Debugging golang/go#64227, golang/vscode-go#3126: + // Assert that the existing metadata graph is acyclic. + if cycle := cyclic(g.Packages); cycle != "" { + bug.Reportf("metadata is cyclic even before updates: %s", cycle) + } + // Assert that the updates contain no self-cycles. + for id, mp := range updates { + if mp != nil { + for _, depID := range mp.DepsByPkgPath { + if depID == id { + bug.Reportf("self-cycle in metadata update: %s", id) + } + } + } + } + // Copy pkgs map then apply updates. pkgs := make(map[PackageID]*Package, len(g.Packages)) for id, mp := range g.Packages { @@ -223,6 +239,43 @@ func breakImportCycles(metadata, updates map[PackageID]*Package) { } } +// cyclic returns a description of a cycle, +// if the graph is cyclic, otherwise "". +func cyclic(graph map[PackageID]*Package) string { + const ( + unvisited = 0 + visited = 1 + onstack = 2 + ) + color := make(map[PackageID]int) + var visit func(id PackageID) string + visit = func(id PackageID) string { + switch color[id] { + case unvisited: + color[id] = onstack + case onstack: + return string(id) // cycle! + case visited: + return "" + } + if mp := graph[id]; mp != nil { + for _, depID := range mp.DepsByPkgPath { + if cycle := visit(depID); cycle != "" { + return string(id) + "->" + cycle + } + } + } + color[id] = visited + return "" + } + for id := range graph { + if cycle := visit(id); cycle != "" { + return cycle + } + } + return "" +} + // detectImportCycles reports cycles in the metadata graph. It returns a new // unordered array of all cycles (nontrivial strong components) in the // metadata graph reachable from a non-nil 'updates' value. diff --git a/gopls/internal/lsp/cache/metadata/metadata.go b/gopls/internal/cache/metadata/metadata.go similarity index 99% rename from gopls/internal/lsp/cache/metadata/metadata.go rename to gopls/internal/cache/metadata/metadata.go index 922605c0eed..b6355166640 100644 --- a/gopls/internal/lsp/cache/metadata/metadata.go +++ b/gopls/internal/cache/metadata/metadata.go @@ -20,7 +20,7 @@ import ( "strings" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/packagesinternal" ) diff --git a/gopls/internal/lsp/cache/methodsets/methodsets.go b/gopls/internal/cache/methodsets/methodsets.go similarity index 100% rename from gopls/internal/lsp/cache/methodsets/methodsets.go rename to gopls/internal/cache/methodsets/methodsets.go diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/cache/mod.go similarity index 98% rename from gopls/internal/lsp/cache/mod.go rename to gopls/internal/cache/mod.go index 6d83166cfbe..a120037e221 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/cache/mod.go @@ -15,8 +15,8 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/mod/module" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" @@ -350,7 +350,7 @@ func (s *Snapshot) extractGoCommandErrors(ctx context.Context, goCmdError error) // file/position information, so don't even try to find it. continue } - loc, found, err := s.matchErrorToModule(ctx, pm, msg) + loc, found, err := s.matchErrorToModule(pm, msg) if err != nil { event.Error(ctx, "matching error to module", err) continue @@ -395,7 +395,7 @@ var moduleVersionInErrorRe = regexp.MustCompile(`[:\s]([+-._~0-9A-Za-z]+)@([+-._ // // It returns the location of a reference to the one of the modules and true // if one exists. If none is found it returns a fallback location and false. -func (s *Snapshot) matchErrorToModule(ctx context.Context, pm *ParsedModule, goCmdError string) (protocol.Location, bool, error) { +func (s *Snapshot) matchErrorToModule(pm *ParsedModule, goCmdError string) (protocol.Location, bool, error) { var reference *modfile.Line matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/cache/mod_tidy.go similarity index 99% rename from gopls/internal/lsp/cache/mod_tidy.go rename to gopls/internal/cache/mod_tidy.go index 67c6d64549a..6dbe9820182 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/cache/mod_tidy.go @@ -17,8 +17,8 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" @@ -66,7 +66,7 @@ func (s *Snapshot) ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule if err != nil { return nil, err } - if _, ok := fh.(*Overlay); ok { + if _, ok := fh.(*overlay); ok { if info, _ := os.Stat(uri.Path()); info == nil { return nil, ErrNoModOnDisk } diff --git a/gopls/internal/lsp/cache/mod_vuln.go b/gopls/internal/cache/mod_vuln.go similarity index 99% rename from gopls/internal/lsp/cache/mod_vuln.go rename to gopls/internal/cache/mod_vuln.go index 5704863d94f..a92f5b5abe1 100644 --- a/gopls/internal/lsp/cache/mod_vuln.go +++ b/gopls/internal/cache/mod_vuln.go @@ -16,8 +16,8 @@ import ( "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/gopls/internal/vulncheck/govulncheck" "golang.org/x/tools/gopls/internal/vulncheck/osv" diff --git a/gopls/internal/lsp/cache/os_darwin.go b/gopls/internal/cache/os_darwin.go similarity index 100% rename from gopls/internal/lsp/cache/os_darwin.go rename to gopls/internal/cache/os_darwin.go diff --git a/gopls/internal/lsp/cache/os_windows.go b/gopls/internal/cache/os_windows.go similarity index 100% rename from gopls/internal/lsp/cache/os_windows.go rename to gopls/internal/cache/os_windows.go diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/cache/parse.go similarity index 96% rename from gopls/internal/lsp/cache/parse.go rename to gopls/internal/cache/parse.go index 0f93a08d4a8..c8da20eed13 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/cache/parse.go @@ -12,7 +12,7 @@ import ( "path/filepath" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/parsego" ) // ParseGo parses the file whose contents are provided by fh. diff --git a/gopls/internal/lsp/cache/parse_cache.go b/gopls/internal/cache/parse_cache.go similarity index 99% rename from gopls/internal/lsp/cache/parse_cache.go rename to gopls/internal/cache/parse_cache.go index 0e5160c593f..55eced51403 100644 --- a/gopls/internal/lsp/cache/parse_cache.go +++ b/gopls/internal/cache/parse_cache.go @@ -18,8 +18,8 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/cache/parsego" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/tokeninternal" ) diff --git a/gopls/internal/lsp/cache/parse_cache_test.go b/gopls/internal/cache/parse_cache_test.go similarity index 99% rename from gopls/internal/lsp/cache/parse_cache_test.go rename to gopls/internal/cache/parse_cache_test.go index 61a204d5d20..eee7ded39af 100644 --- a/gopls/internal/lsp/cache/parse_cache_test.go +++ b/gopls/internal/cache/parse_cache_test.go @@ -13,7 +13,7 @@ import ( "time" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" ) func skipIfNoParseCache(t *testing.T) { diff --git a/gopls/internal/lsp/cache/parsego/file.go b/gopls/internal/cache/parsego/file.go similarity index 98% rename from gopls/internal/lsp/cache/parsego/file.go rename to gopls/internal/cache/parsego/file.go index 6f47ca35580..3e13d5b2c43 100644 --- a/gopls/internal/lsp/cache/parsego/file.go +++ b/gopls/internal/cache/parsego/file.go @@ -10,7 +10,7 @@ import ( "go/scanner" "go/token" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" ) diff --git a/gopls/internal/lsp/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go similarity index 99% rename from gopls/internal/lsp/cache/parsego/parse.go rename to gopls/internal/cache/parsego/parse.go index 575699f6e48..89fc15fb279 100644 --- a/gopls/internal/lsp/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -14,7 +14,7 @@ import ( "go/token" "reflect" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/diff" diff --git a/gopls/internal/lsp/cache/parsego/parse_test.go b/gopls/internal/cache/parsego/parse_test.go similarity index 95% rename from gopls/internal/lsp/cache/parsego/parse_test.go rename to gopls/internal/cache/parsego/parse_test.go index 0c8f00b0c8c..4018e9ed886 100644 --- a/gopls/internal/lsp/cache/parsego/parse_test.go +++ b/gopls/internal/cache/parsego/parse_test.go @@ -10,7 +10,7 @@ import ( "go/token" "testing" - "golang.org/x/tools/gopls/internal/lsp/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/tokeninternal" ) diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/cache/pkg.go similarity index 81% rename from gopls/internal/lsp/cache/pkg.go rename to gopls/internal/cache/pkg.go index 19b974f90c2..821b1cc48e8 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/cache/pkg.go @@ -5,7 +5,6 @@ package cache import ( - "context" "fmt" "go/ast" "go/scanner" @@ -13,29 +12,23 @@ import ( "go/types" "sync" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/cache/methodsets" - "golang.org/x/tools/gopls/internal/lsp/cache/parsego" - "golang.org/x/tools/gopls/internal/lsp/cache/xrefs" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/methodsets" + "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/xrefs" + "golang.org/x/tools/gopls/internal/protocol" ) -// Temporary refactoring, reversing the source import: -// Types +// Convenient aliases for very heavily used types. type ( - // Metadata. - PackageID = metadata.PackageID - PackagePath = metadata.PackagePath - PackageName = metadata.PackageName - ImportPath = metadata.ImportPath - - // Computed objects. + PackageID = metadata.PackageID + PackagePath = metadata.PackagePath + PackageName = metadata.PackageName + ImportPath = metadata.ImportPath ParsedGoFile = parsego.File ) -// Values -var ( - // Parse Modes +const ( ParseHeader = parsego.ParseHeader ParseFull = parsego.ParseFull ) @@ -172,19 +165,3 @@ func (p *Package) GetParseErrors() []scanner.ErrorList { func (p *Package) GetTypeErrors() []types.Error { return p.pkg.typeErrors } - -func (p *Package) DiagnosticsForFile(ctx context.Context, uri protocol.DocumentURI) ([]*Diagnostic, error) { - var diags []*Diagnostic - for _, diag := range p.loadDiagnostics { - if diag.URI == uri { - diags = append(diags, diag) - } - } - for _, diag := range p.pkg.diagnostics { - if diag.URI == uri { - diags = append(diags, diag) - } - } - - return diags, nil -} diff --git a/gopls/internal/lsp/cache/port.go b/gopls/internal/cache/port.go similarity index 100% rename from gopls/internal/lsp/cache/port.go rename to gopls/internal/cache/port.go diff --git a/gopls/internal/lsp/cache/port_test.go b/gopls/internal/cache/port_test.go similarity index 98% rename from gopls/internal/lsp/cache/port_test.go rename to gopls/internal/cache/port_test.go index 96ba31846f8..3c38a1184f0 100644 --- a/gopls/internal/lsp/cache/port_test.go +++ b/gopls/internal/cache/port_test.go @@ -12,7 +12,7 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/cache/session.go similarity index 90% rename from gopls/internal/lsp/cache/session.go rename to gopls/internal/cache/session.go index bf8cc54c0e4..7d2bf43773f 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/cache/session.go @@ -15,13 +15,15 @@ import ( "strings" "sync" "sync/atomic" + "time" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/typerefs" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/cache/typerefs" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/persistent" + "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" @@ -30,6 +32,25 @@ import ( "golang.org/x/tools/internal/xcontext" ) +// NewSession creates a new gopls session with the given cache. +func NewSession(ctx context.Context, c *Cache) *Session { + index := atomic.AddInt64(&sessionIndex, 1) + s := &Session{ + id: strconv.FormatInt(index, 10), + cache: c, + gocmdRunner: &gocommand.Runner{}, + overlayFS: newOverlayFS(c), + parseCache: newParseCache(1 * time.Minute), // keep recently parsed files for a minute, to optimize typing CPU + viewMap: make(map[protocol.DocumentURI]*View), + } + event.Log(ctx, "New session", KeyCreateSession.Of(s)) + return s +} + +// A Session holds the state (views, file contents, parse cache, +// memoized computations) of a gopls server process. +// +// It implements the file.Source interface. type Session struct { // Unique identifier for this session. id string @@ -170,6 +191,33 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, * ignoreFilter = newIgnoreFilter(dirs) } + var pe *imports.ProcessEnv + { + env := make(map[string]string) + envSlice := slices.Concat(os.Environ(), def.folder.Options.EnvSlice(), []string{"GO111MODULE=" + def.adjustedGO111MODULE()}) + for _, kv := range envSlice { + if k, v, ok := strings.Cut(kv, "="); ok { + env[k] = v + } + } + pe = &imports.ProcessEnv{ + GocmdRunner: s.gocmdRunner, + BuildFlags: slices.Clone(def.folder.Options.BuildFlags), + // TODO(rfindley): an old comment said "processEnv operations should not mutate the modfile" + // But shouldn't we honor the default behavior of mod vendoring? + ModFlag: "readonly", + SkipPathInScan: skipPath, + Env: env, + WorkingDir: def.root.Path(), + ModCache: s.cache.modCache.dirCache(def.folder.Env.GOMODCACHE), + } + if def.folder.Options.VerboseOutput { + pe.Logf = func(format string, args ...interface{}) { + event.Log(ctx, fmt.Sprintf(format, args...)) + } + } + } + v := &View{ id: strconv.FormatInt(index, 10), gocmdRunner: s.gocmdRunner, @@ -180,13 +228,7 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, * ignoreFilter: ignoreFilter, fs: s.overlayFS, viewDefinition: def, - importsState: &importsState{ - ctx: backgroundCtx, - processEnv: &imports.ProcessEnv{ - GocmdRunner: s.gocmdRunner, - SkipPathInScan: skipPath, - }, - }, + importsState: newImportsState(backgroundCtx, s.cache.modCache, pe), } s.snapshotWG.Add(1) @@ -438,7 +480,7 @@ func selectViewDefs(ctx context.Context, fs file.Source, folders []*Folder, open checkFiles: for _, uri := range openFiles { folder := folderForFile(uri) - if folder == nil { + if folder == nil || !folder.Options.ZeroConfig { continue // only guess views for open files } fh, err := fs.ReadFile(ctx, uri) @@ -548,7 +590,7 @@ func bestView[V viewDefiner](ctx context.Context, fs file.Source, fh file.Handle pushView(&workViews, view) } case GoModView: - if modURI == def.gomod { + if _, ok := def.workspaceModFiles[modURI]; ok { modViews = append(modViews, view) } case GOPATHView: @@ -611,7 +653,7 @@ func bestView[V viewDefiner](ctx context.Context, fs file.Source, fh file.Handle // // If the resulting error is non-nil, the view may or may not have already been // dropped from the session. -func (s *Session) updateViewLocked(ctx context.Context, view *View, def *viewDefinition, folder *Folder) (*View, error) { +func (s *Session) updateViewLocked(ctx context.Context, view *View, def *viewDefinition) (*View, error) { i := s.dropView(view) if i == -1 { return nil, fmt.Errorf("view %q not found", view.id) @@ -663,7 +705,7 @@ func (s *Session) ResetView(ctx context.Context, uri protocol.DocumentURI) (*Vie if err != nil { return nil, err } - return s.updateViewLocked(ctx, v, v.viewDefinition, v.folder) + return s.updateViewLocked(ctx, v, v.viewDefinition) } // DidModifyFiles reports a file modification to the session. It returns @@ -675,7 +717,7 @@ func (s *Session) ResetView(ctx context.Context, uri protocol.DocumentURI) (*Vie // TODO(rfindley): what happens if this function fails? It must leave us in a // broken state, which we should surface to the user, probably as a request to // restart gopls. -func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modification) (map[*View][]protocol.DocumentURI, error) { +func (s *Session) DidModifyFiles(ctx context.Context, modifications []file.Modification) (map[*View][]protocol.DocumentURI, error) { s.viewMu.Lock() defer s.viewMu.Unlock() @@ -684,7 +726,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modificatio // This is done while holding viewMu because the set of open files affects // the set of views, and to prevent views from seeing updated file content // before they have processed invalidations. - replaced, err := s.updateOverlays(ctx, changes) + replaced, err := s.updateOverlays(ctx, modifications) if err != nil { return nil, err } @@ -695,7 +737,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modificatio checkViews := false changed := make(map[protocol.DocumentURI]file.Handle) - for _, c := range changes { + for _, c := range modifications { fh := mustReadFile(ctx, s, c.URI) changed[c.URI] = fh @@ -704,18 +746,12 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modificatio checkViews = true } - // Any on-disk change to a go.work file causes recomputing views. + // Any on-disk change to a go.work or go.mod file causes recomputing views. // // TODO(rfindley): go.work files need not be named "go.work" -- we need to // check each view's source to handle the case of an explicit GOWORK value. // Write a test that fails, and fix this. - if isGoWork(c.URI) && (c.Action == file.Save || c.OnDisk) { - checkViews = true - } - // Opening/Close/Create/Delete of go.mod files all trigger - // re-evaluation of Views. Changes do not as they can't affect the set of - // Views. - if isGoMod(c.URI) && c.Action != file.Change && c.Action != file.Save { + if (isGoWork(c.URI) || isGoMod(c.URI)) && (c.Action == file.Save || c.OnDisk) { checkViews = true } @@ -733,7 +769,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modificatio // branch change. Be careful to only do this if both files are open Go // files. if old, ok := replaced[c.URI]; ok && !checkViews && fileKind(fh) == file.Go { - if new, ok := fh.(*Overlay); ok { + if new, ok := fh.(*overlay); ok { if buildComment(old.content) != buildComment(new.content) { checkViews = true } @@ -813,7 +849,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modificatio // We only want to run fast-path diagnostics (i.e. diagnoseChangedFiles) once // for each changed file, in its best view. viewsToDiagnose := map[*View][]protocol.DocumentURI{} - for _, mod := range changes { + for _, mod := range modifications { v, err := s.viewOfLocked(ctx, mod.URI) if err != nil { // bestViewForURI only returns an error in the event of context @@ -830,7 +866,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modificatio // ...but changes may be relevant to other views, for example if they are // changes to a shared package. for _, v := range s.views { - _, release, needsDiagnosis := s.invalidateViewLocked(ctx, v, StateChange{Files: changed}) + _, release, needsDiagnosis := s.invalidateViewLocked(ctx, v, StateChange{Modifications: modifications, Files: changed}) release() if needsDiagnosis || checkViews { @@ -894,11 +930,11 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes // // Precondition: caller holds s.viewMu lock. // TODO(rfindley): move this to fs_overlay.go. -func (fs *overlayFS) updateOverlays(ctx context.Context, changes []file.Modification) (map[protocol.DocumentURI]*Overlay, error) { +func (fs *overlayFS) updateOverlays(ctx context.Context, changes []file.Modification) (map[protocol.DocumentURI]*overlay, error) { fs.mu.Lock() defer fs.mu.Unlock() - replaced := make(map[protocol.DocumentURI]*Overlay) + replaced := make(map[protocol.DocumentURI]*overlay) for _, c := range changes { o, ok := fs.overlays[c.URI] if ok { @@ -964,7 +1000,7 @@ func (fs *overlayFS) updateOverlays(ctx context.Context, changes []file.Modifica _, readErr := fh.Content() sameContentOnDisk = (readErr == nil && fh.Identity().Hash == hash) } - o = &Overlay{ + o = &overlay{ uri: c.URI, version: version, content: text, @@ -1005,9 +1041,9 @@ func (b brokenFile) SameContentsOnDisk() bool { return false } func (b brokenFile) Version() int32 { return 0 } func (b brokenFile) Content() ([]byte, error) { return nil, b.err } -// FileWatchingGlobPatterns returns a set of glob patterns patterns that the -// client is required to watch for changes, and notify the server of them, in -// order to keep the server's state up to date. +// FileWatchingGlobPatterns returns a set of glob patterns that the client is +// required to watch for changes, and notify the server of them, in order to +// keep the server's state up to date. // // This set includes // 1. all go.mod and go.work files in the workspace; and @@ -1023,17 +1059,27 @@ func (b brokenFile) Content() ([]byte, error) { return nil, b.err } // The watch for workspace directories in (2) should keep each View up to date, // as it should capture any newly added/modified/deleted Go files. // +// Patterns are returned as a set of protocol.RelativePatterns, since they can +// always be later translated to glob patterns (i.e. strings) if the client +// lacks relative pattern support. By convention, any pattern returned with +// empty baseURI should be served as a glob pattern. +// +// In general, we prefer to serve relative patterns, as they work better on +// most clients that support both, and do not have issues with Windows driver +// letter casing: +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#relativePattern +// // TODO(golang/go#57979): we need to reset the memoizedFS when a view changes. // Consider the case where we incidentally read a file, then it moved outside // of an active module, and subsequently changed: we would still observe the // original file state. -func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]unit { +func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[protocol.RelativePattern]unit { s.viewMu.Lock() defer s.viewMu.Unlock() // Always watch files that may change the set of views. - patterns := map[string]unit{ - "**/*.{mod,work}": {}, + patterns := map[protocol.RelativePattern]unit{ + {Pattern: "**/*.{mod,work}"}: {}, } for _, view := range s.views { @@ -1041,7 +1087,7 @@ func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]unit if err != nil { continue // view is shut down; continue with others } - for k, v := range snapshot.fileWatchingGlobPatterns(ctx) { + for k, v := range snapshot.fileWatchingGlobPatterns() { patterns[k] = v } release() @@ -1061,7 +1107,7 @@ func (s *Session) OrphanedFileDiagnostics(ctx context.Context) (map[protocol.Doc // funcs. diagnostics := make(map[protocol.DocumentURI][]*Diagnostic) - byView := make(map[*View][]*Overlay) + byView := make(map[*View][]*overlay) for _, o := range s.Overlays() { uri := o.URI() snapshot, release, err := s.SnapshotOf(ctx, uri) diff --git a/gopls/internal/lsp/cache/session_test.go b/gopls/internal/cache/session_test.go similarity index 75% rename from gopls/internal/lsp/cache/session_test.go rename to gopls/internal/cache/session_test.go index 11046a21214..a3bd8ce5800 100644 --- a/gopls/internal/lsp/cache/session_test.go +++ b/gopls/internal/cache/session_test.go @@ -12,12 +12,15 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/test/integration/fake" + "golang.org/x/tools/internal/testenv" ) func TestZeroConfigAlgorithm(t *testing.T) { + testenv.NeedsExec(t) // executes the Go command + type viewSummary struct { // fields exported for cmp.Diff Type ViewType @@ -224,6 +227,91 @@ func TestZeroConfigAlgorithm(t *testing.T) { []string{"a/a.go", "b/b.go", "b/c/c.go"}, []viewSummary{{GoWorkView, ".", nil}, {GoModView, "b/c", []string{"GOWORK=off"}}}, }, + { + "go.mod with nested replace", + map[string]string{ + "go.mod": "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b", + "a.go": "package a", + "b/go.mod": "module golang.org/b\ngo 1.18\n", + "b/b.go": "package b", + }, + []folderSummary{{dir: "."}}, + []string{"a/a.go", "b/b.go"}, + []viewSummary{{GoModView, ".", nil}}, + }, + { + "go.mod with parent replace, parent folder", + map[string]string{ + "go.mod": "module golang.org/a", + "a.go": "package a", + "b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../", + "b/b.go": "package b", + }, + []folderSummary{{dir: "."}}, + []string{"a/a.go", "b/b.go"}, + []viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}}, + }, + { + "go.mod with multiple replace", + map[string]string{ + "go.mod": ` +module golang.org/root + +require ( + golang.org/a v1.2.3 + golang.org/b v1.2.3 + golang.org/c v1.2.3 +) + +replace ( + golang.org/b => ./b + golang.org/c => ./c + // Note: d is not replaced +) +`, + "a.go": "package a", + "b/go.mod": "module golang.org/b\ngo 1.18", + "b/b.go": "package b", + "c/go.mod": "module golang.org/c\ngo 1.18", + "c/c.go": "package c", + "d/go.mod": "module golang.org/d\ngo 1.18", + "d/d.go": "package d", + }, + []folderSummary{{dir: "."}}, + []string{"b/b.go", "c/c.go", "d/d.go"}, + []viewSummary{{GoModView, ".", nil}, {GoModView, "d", nil}}, + }, + { + "go.mod with many replace", + map[string]string{ + "go.mod": "module golang.org/a\ngo 1.18", + "a.go": "package a", + "b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../", + "b/b.go": "package b", + }, + []folderSummary{{dir: "b"}}, + []string{"a/a.go", "b/b.go"}, + []viewSummary{{GoModView, "b", nil}}, + }, + { + "go.mod with replace directive; workspace replace off", + map[string]string{ + "go.mod": "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b", + "a.go": "package a", + "b/go.mod": "module golang.org/b\ngo 1.18\n", + "b/b.go": "package b", + }, + []folderSummary{{ + dir: ".", + options: func(string) map[string]any { + return map[string]any{ + "includeReplaceInWorkspace": false, + } + }, + }}, + []string{"a/a.go", "b/b.go"}, + []viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}}, + }, } for _, test := range tests { @@ -250,7 +338,7 @@ func TestZeroConfigAlgorithm(t *testing.T) { } env, err := FetchGoEnv(ctx, toURI(f.dir), opts) if err != nil { - t.Fatalf("fetching env: %v", env) + t.Fatalf("FetchGoEnv failed: %v", err) } folders = append(folders, &Folder{ Dir: toURI(f.dir), diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/cache/snapshot.go similarity index 94% rename from gopls/internal/lsp/cache/snapshot.go rename to gopls/internal/cache/snapshot.go index 83e02d27c3d..3d97ed47ccb 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -16,6 +16,7 @@ import ( "go/types" "io" "os" + "path" "path/filepath" "regexp" "runtime" @@ -27,14 +28,14 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/methodsets" + "golang.org/x/tools/gopls/internal/cache/typerefs" + "golang.org/x/tools/gopls/internal/cache/xrefs" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/filecache" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/cache/methodsets" - "golang.org/x/tools/gopls/internal/lsp/cache/typerefs" - "golang.org/x/tools/gopls/internal/lsp/cache/xrefs" - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/constraints" @@ -240,7 +241,7 @@ func (s *Snapshot) decref() { if s.refcount == 0 { s.packages.Destroy() s.activePackages.Destroy() - s.files.Destroy() + s.files.destroy() s.symbolizeHandles.Destroy() s.parseModHandles.Destroy() s.parseWorkHandles.Destroy() @@ -313,7 +314,7 @@ func fileKind(fh file.Handle) file.Kind { // The kind of an unsaved buffer comes from the // TextDocumentItem.LanguageID field in the didChange event, // not from the file name. They may differ. - if o, ok := fh.(*Overlay); ok { + if o, ok := fh.(*overlay); ok { if o.kind != file.UnknownKind { return o.kind } @@ -350,7 +351,7 @@ func (s *Snapshot) Templates() map[protocol.DocumentURI]file.Handle { defer s.mu.Unlock() tmpls := map[protocol.DocumentURI]file.Handle{} - s.files.Range(func(k protocol.DocumentURI, fh file.Handle) { + s.files.foreach(func(k protocol.DocumentURI, fh file.Handle) { if s.FileKind(fh) == file.Tmpl { tmpls[k] = fh } @@ -546,51 +547,17 @@ func (s *Snapshot) goCommandInvocation(ctx context.Context, flags InvocationFlag // // TODO(rfindley): should we set -overlays here? - var modURI protocol.DocumentURI - // Select the module context to use. - // If we're type checking, we need to use the workspace context, meaning - // the main (workspace) module. Otherwise, we should use the module for - // the passed-in working dir. - if mode == LoadWorkspace { - // TODO(rfindley): this seems unnecessary and overly complicated. Remove - // this along with 'allowModFileModifications'. - if s.view.typ == GoModView { - modURI = s.view.gomod - } - } else { - modURI = s.GoModForFile(protocol.URIFromPath(inv.WorkingDir)) - } - - var modContent []byte - if modURI != "" { - modFH, err := s.ReadFile(ctx, modURI) - if err != nil { - return "", nil, cleanup, err - } - modContent, err = modFH.Content() - if err != nil { - return "", nil, cleanup, err - } - } - - // TODO(rfindley): in the case of go.work mode, modURI is empty and we fall - // back on the default behavior of vendorEnabled with an empty modURI. Figure - // out what is correct here and implement it explicitly. - vendorEnabled, err := s.vendorEnabled(ctx, modURI, modContent) - if err != nil { - return "", nil, cleanup, err - } - const mutableModFlag = "mod" + // If the mod flag isn't set, populate it based on the mode and workspace. + // + // (As noted in various TODOs throughout this function, this is very + // confusing and not obviously correct, but tests pass and we will eventually + // rewrite this entire function.) if inv.ModFlag == "" { switch mode { case LoadWorkspace, Normal: - if vendorEnabled { - inv.ModFlag = "vendor" - } else if !allowModfileModificationOption { - inv.ModFlag = "readonly" - } else { + if allowModfileModificationOption { inv.ModFlag = mutableModFlag } case WriteTemporaryModFile: @@ -607,6 +574,32 @@ func (s *Snapshot) goCommandInvocation(ctx context.Context, flags InvocationFlag // If the invocation needs to mutate the modfile, we must use a temp mod. if inv.ModFlag == mutableModFlag { + var modURI protocol.DocumentURI + // Select the module context to use. + // If we're type checking, we need to use the workspace context, meaning + // the main (workspace) module. Otherwise, we should use the module for + // the passed-in working dir. + if mode == LoadWorkspace { + // TODO(rfindley): this seems unnecessary and overly complicated. Remove + // this along with 'allowModFileModifications'. + if s.view.typ == GoModView { + modURI = s.view.gomod + } + } else { + modURI = s.GoModForFile(protocol.URIFromPath(inv.WorkingDir)) + } + + var modContent []byte + if modURI != "" { + modFH, err := s.ReadFile(ctx, modURI) + if err != nil { + return "", nil, cleanup, err + } + modContent, err = modFH.Content() + if err != nil { + return "", nil, cleanup, err + } + } if modURI == "" { return "", nil, cleanup, fmt.Errorf("no go.mod file found in %s", inv.WorkingDir) } @@ -640,11 +633,11 @@ func (s *Snapshot) buildOverlay() map[string][]byte { // // Note that this may differ from the set of overlays on the server, if the // snapshot observed a historical state. -func (s *Snapshot) Overlays() []*Overlay { +func (s *Snapshot) Overlays() []*overlay { s.mu.Lock() defer s.mu.Unlock() - return s.files.Overlays() + return s.files.getOverlays() } // Package data kinds, identifying various package data that may be stored in @@ -697,15 +690,15 @@ func (s *Snapshot) PackageDiagnostics(ctx context.Context, ids ...PackageID) (ma // // If these indexes cannot be loaded from cache, the requested packages may // be type-checked. -func (s *Snapshot) References(ctx context.Context, ids ...PackageID) ([]XrefIndex, error) { +func (s *Snapshot) References(ctx context.Context, ids ...PackageID) ([]xrefIndex, error) { ctx, done := event.Start(ctx, "cache.snapshot.References") defer done() - indexes := make([]XrefIndex, len(ids)) + indexes := make([]xrefIndex, len(ids)) pre := func(i int, ph *packageHandle) bool { data, err := filecache.Get(xrefsKind, ph.key) if err == nil { // hit - indexes[i] = XrefIndex{mp: ph.mp, data: data} + indexes[i] = xrefIndex{mp: ph.mp, data: data} return false } else if err != filecache.ErrNotFound { event.Error(ctx, "reading xrefs from filecache", err) @@ -713,18 +706,18 @@ func (s *Snapshot) References(ctx context.Context, ids ...PackageID) ([]XrefInde return true } post := func(i int, pkg *Package) { - indexes[i] = XrefIndex{mp: pkg.metadata, data: pkg.pkg.xrefs()} + indexes[i] = xrefIndex{mp: pkg.metadata, data: pkg.pkg.xrefs()} } return indexes, s.forEachPackage(ctx, ids, pre, post) } -// An XrefIndex is a helper for looking up references in a given package. -type XrefIndex struct { +// An xrefIndex is a helper for looking up references in a given package. +type xrefIndex struct { mp *metadata.Package data []byte } -func (index XrefIndex) Lookup(targets map[PackagePath]map[objectpath.Path]struct{}) []protocol.Location { +func (index xrefIndex) Lookup(targets map[PackagePath]map[objectpath.Path]struct{}) []protocol.Location { return xrefs.Lookup(index.mp, index.data, targets) } @@ -938,23 +931,33 @@ func (s *Snapshot) resetActivePackagesLocked() { // See Session.FileWatchingGlobPatterns for a description of gopls' file // watching heuristic. -func (s *Snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]struct{} { - extensions := "go,mod,sum,work" - for _, ext := range s.Options().TemplateExtensions { - extensions += "," + ext - } - +func (s *Snapshot) fileWatchingGlobPatterns() map[protocol.RelativePattern]unit { // Always watch files that may change the view definition. - patterns := make(map[string]unit) + patterns := make(map[protocol.RelativePattern]unit) // If GOWORK is outside the folder, ensure we are watching it. if s.view.gowork != "" && !s.view.folder.Dir.Encloses(s.view.gowork) { - // TODO(rfindley): use RelativePatterns here as well (see below). - patterns[filepath.ToSlash(s.view.gowork.Path())] = unit{} + workPattern := protocol.RelativePattern{ + BaseURI: s.view.gowork.Dir(), + Pattern: path.Base(string(s.view.gowork)), + } + patterns[workPattern] = unit{} + } + + extensions := "go,mod,sum,work" + for _, ext := range s.Options().TemplateExtensions { + extensions += "," + ext } + watchGoFiles := fmt.Sprintf("**/*.{%s}", extensions) var dirs []string if s.view.moduleMode() { + if s.view.typ == GoWorkView { + workVendorDir := filepath.Join(s.view.gowork.Dir().Path(), "vendor") + workVendorURI := protocol.URIFromPath(workVendorDir) + patterns[protocol.RelativePattern{BaseURI: workVendorURI, Pattern: watchGoFiles}] = unit{} + } + // In module mode, watch directories containing active modules, and collect // these dirs for later filtering the set of known directories. // @@ -964,21 +967,17 @@ func (s *Snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru dir := filepath.Dir(modFile.Path()) dirs = append(dirs, dir) - // TODO(golang/go#64763): Switch to RelativePatterns if RelativePatternSupport - // is available. Relative patterns do not have issues with Windows drive - // letter casing. - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#relativePattern - // - // TODO(golang/go#64724): thoroughly test, particularly on on Windows. + // TODO(golang/go#64724): thoroughly test these patterns, particularly on + // on Windows. // // Note that glob patterns should use '/' on Windows: // https://code.visualstudio.com/docs/editor/glob-patterns - patterns[fmt.Sprintf("%s/**/*.{%s}", filepath.ToSlash(dir), extensions)] = unit{} + patterns[protocol.RelativePattern{BaseURI: modFile.Dir(), Pattern: watchGoFiles}] = unit{} } } else { // In non-module modes (GOPATH or AdHoc), we just watch the workspace root. dirs = []string{s.view.root.Path()} - patterns[fmt.Sprintf("**/*.{%s}", extensions)] = unit{} + patterns[protocol.RelativePattern{Pattern: watchGoFiles}] = unit{} } if s.watchSubdirs() { @@ -1007,14 +1006,14 @@ func (s *Snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru return patterns } -func (s *Snapshot) addKnownSubdirs(patterns map[string]unit, wsDirs []string) { +func (s *Snapshot) addKnownSubdirs(patterns map[protocol.RelativePattern]unit, wsDirs []string) { s.mu.Lock() defer s.mu.Unlock() - s.files.Dirs().Range(func(dir string) { + s.files.getDirs().Range(func(dir string) { for _, wsDir := range wsDirs { if pathutil.InDir(wsDir, dir) { - patterns[filepath.ToSlash(dir)] = unit{} + patterns[protocol.RelativePattern{Pattern: filepath.ToSlash(dir)}] = unit{} } } }) @@ -1055,11 +1054,11 @@ func (s *Snapshot) filesInDir(uri protocol.DocumentURI) []protocol.DocumentURI { defer s.mu.Unlock() dir := uri.Path() - if !s.files.Dirs().Contains(dir) { + if !s.files.getDirs().Contains(dir) { return nil } var files []protocol.DocumentURI - s.files.Range(func(uri protocol.DocumentURI, _ file.Handle) { + s.files.foreach(func(uri protocol.DocumentURI, _ file.Handle) { if pathutil.InDir(dir, uri.Path()) { files = append(files, uri) } @@ -1261,7 +1260,7 @@ func (s *Snapshot) FindFile(uri protocol.DocumentURI) file.Handle { s.mu.Lock() defer s.mu.Unlock() - result, _ := s.files.Get(uri) + result, _ := s.files.get(uri) return result } @@ -1274,14 +1273,14 @@ func (s *Snapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file s.mu.Lock() defer s.mu.Unlock() - fh, ok := s.files.Get(uri) + fh, ok := s.files.get(uri) if !ok { var err error fh, err = s.view.fs.ReadFile(ctx, uri) if err != nil { return nil, err } - s.files.Set(uri, fh) + s.files.set(uri, fh) } return fh, nil } @@ -1316,8 +1315,8 @@ func (s *Snapshot) preloadFiles(ctx context.Context, uris []protocol.DocumentURI continue // error logged above } uri := uris[i] - if _, ok := s.files.Get(uri); !ok { - s.files.Set(uri, fh) + if _, ok := s.files.get(uri); !ok { + s.files.set(uri, fh) } } } @@ -1327,8 +1326,8 @@ func (s *Snapshot) IsOpen(uri protocol.DocumentURI) bool { s.mu.Lock() defer s.mu.Unlock() - fh, _ := s.files.Get(uri) - _, open := fh.(*Overlay) + fh, _ := s.files.get(uri) + _, open := fh.(*overlay) return open } @@ -1407,13 +1406,13 @@ func (s *Snapshot) reloadWorkspace(ctx context.Context) { } } -func (s *Snapshot) orphanedFileDiagnostics(ctx context.Context, overlays []*Overlay) ([]*Diagnostic, error) { +func (s *Snapshot) orphanedFileDiagnostics(ctx context.Context, overlays []*overlay) ([]*Diagnostic, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } var diagnostics []*Diagnostic - var orphaned []*Overlay + var orphaned []*overlay searchOverlays: for _, o := range overlays { uri := o.URI() @@ -1603,7 +1602,7 @@ https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-str Message: msg, SuggestedFixes: suggestedFixes, } - if ok := BundleQuickFixes(d); !ok { + if ok := bundleQuickFixes(d); !ok { bug.Reportf("failed to bundle quick fixes for %v", d) } // Only report diagnostics if we detect an actual exclusion. @@ -1692,7 +1691,7 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f initialErr: s.initialErr, packages: s.packages.Clone(), activePackages: s.activePackages.Clone(), - files: s.files.Clone(changedFiles), + files: s.files.clone(changedFiles), symbolizeHandles: cloneWithout(s.symbolizeHandles, changedFiles, nil), workspacePackages: s.workspacePackages, shouldLoad: s.shouldLoad.Clone(), // not cloneWithout: shouldLoad is cleared on loads @@ -1735,10 +1734,14 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f // one or more modules may have moved into or out of the // vendor tree after 'go mod vendor' or 'rm -fr vendor/'. // + // In this case, we consider the actual modification to see if was a creation + // or deletion. + // // TODO(rfindley): revisit the location of this check. - for uri := range changedFiles { - if inVendor(uri) && s.initialErr != nil || - strings.HasSuffix(string(uri), "/vendor/modules.txt") { + for _, mod := range changed.Modifications { + if inVendor(mod.URI) && (mod.Action == file.Create || mod.Action == file.Delete) || + strings.HasSuffix(string(mod.URI), "/vendor/modules.txt") { + reinit = true break } @@ -1748,9 +1751,14 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f // they exist. Importantly, we don't call ReadFile here: consider the case // where a file is added on disk; we don't want to read the newly added file // into the old snapshot, as that will break our change detection below. + // + // TODO(rfindley): it may be more accurate to rely on the modification type + // here, similarly to what we do for vendored files above. If we happened not + // to have read a file in the previous snapshot, that's not the same as it + // actually being created. oldFiles := make(map[protocol.DocumentURI]file.Handle) for uri := range changedFiles { - if fh, ok := s.files.Get(uri); ok { + if fh, ok := s.files.get(uri); ok { oldFiles[uri] = fh } } @@ -1820,8 +1828,8 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f for uri, newFH := range changedFiles { // The original FileHandle for this URI is cached on the snapshot. oldFH := oldFiles[uri] // may be nil - _, oldOpen := oldFH.(*Overlay) - _, newOpen := newFH.(*Overlay) + _, oldOpen := oldFH.(*overlay) + _, newOpen := newFH.(*overlay) anyFileOpenedOrClosed = anyFileOpenedOrClosed || (oldOpen != newOpen) anyFileAdded = anyFileAdded || (oldFH == nil || !fileExists(oldFH)) && fileExists(newFH) @@ -2130,11 +2138,11 @@ func invalidatedPackageIDs(uri protocol.DocumentURI, known map[protocol.Document // are both overlays, and if the current FileHandle is saved while the original // FileHandle was not saved. func fileWasSaved(originalFH, currentFH file.Handle) bool { - c, ok := currentFH.(*Overlay) + c, ok := currentFH.(*overlay) if !ok || c == nil { return true } - o, ok := originalFH.(*Overlay) + o, ok := originalFH.(*overlay) if !ok || o == nil { return c.saved } diff --git a/gopls/internal/lsp/cache/symbols.go b/gopls/internal/cache/symbols.go similarity index 99% rename from gopls/internal/lsp/cache/symbols.go rename to gopls/internal/cache/symbols.go index d2c9015361e..5dce87df223 100644 --- a/gopls/internal/lsp/cache/symbols.go +++ b/gopls/internal/cache/symbols.go @@ -12,7 +12,7 @@ import ( "strings" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/astutil" ) diff --git a/gopls/internal/lsp/cache/typerefs/doc.go b/gopls/internal/cache/typerefs/doc.go similarity index 98% rename from gopls/internal/lsp/cache/typerefs/doc.go rename to gopls/internal/cache/typerefs/doc.go index 700da5dde26..18042c623bc 100644 --- a/gopls/internal/lsp/cache/typerefs/doc.go +++ b/gopls/internal/cache/typerefs/doc.go @@ -111,7 +111,7 @@ // The [BuildPackageGraph] constructor implements a whole-graph analysis similar // to that which will be implemented by gopls, but for various reasons the // logic for this analysis will eventually live in the -// [golang.org/x/tools/gopls/internal/lsp/cache] package. Nevertheless, +// [golang.org/x/tools/gopls/internal/cache] package. Nevertheless, // BuildPackageGraph and its test serve to verify the syntactic analysis, and // may serve as a proving ground for new optimizations of the whole-graph analysis. // diff --git a/gopls/internal/lsp/cache/typerefs/packageset.go b/gopls/internal/cache/typerefs/packageset.go similarity index 98% rename from gopls/internal/lsp/cache/typerefs/packageset.go rename to gopls/internal/cache/typerefs/packageset.go index 9d2026f3603..29c37cd1c4c 100644 --- a/gopls/internal/lsp/cache/typerefs/packageset.go +++ b/gopls/internal/cache/typerefs/packageset.go @@ -11,7 +11,7 @@ import ( "strings" "sync" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/metadata" ) // PackageIndex stores common data to enable efficient representation of diff --git a/gopls/internal/lsp/cache/typerefs/pkggraph_test.go b/gopls/internal/cache/typerefs/pkggraph_test.go similarity index 96% rename from gopls/internal/lsp/cache/typerefs/pkggraph_test.go rename to gopls/internal/cache/typerefs/pkggraph_test.go index 7c53f40daf3..01cd1a86f0f 100644 --- a/gopls/internal/lsp/cache/typerefs/pkggraph_test.go +++ b/gopls/internal/cache/typerefs/pkggraph_test.go @@ -17,10 +17,10 @@ import ( "sync" "golang.org/x/sync/errgroup" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/cache/parsego" - "golang.org/x/tools/gopls/internal/lsp/cache/typerefs" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/typerefs" + "golang.org/x/tools/gopls/internal/protocol" ) const ( diff --git a/gopls/internal/lsp/cache/typerefs/pkgrefs_test.go b/gopls/internal/cache/typerefs/pkgrefs_test.go similarity index 97% rename from gopls/internal/lsp/cache/typerefs/pkgrefs_test.go rename to gopls/internal/cache/typerefs/pkgrefs_test.go index 4cc8a82f85b..da14b90cba3 100644 --- a/gopls/internal/lsp/cache/typerefs/pkgrefs_test.go +++ b/gopls/internal/cache/typerefs/pkgrefs_test.go @@ -20,10 +20,10 @@ import ( "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/cache/parsego" - "golang.org/x/tools/gopls/internal/lsp/cache/typerefs" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/typerefs" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/testenv" @@ -327,7 +327,7 @@ func (s mapMetadataSource) Metadata(id PackageID) *Metadata { } // This function is a compressed version of snapshot.load from the -// internal/lsp/cache package, for use in testing. +// internal/cache package, for use in testing. // // TODO(rfindley): it may be valuable to extract this logic from the snapshot, // since it is otherwise standalone. @@ -386,7 +386,7 @@ func loadPackages(query string, needExport bool) (map[PackageID]string, Metadata for importPath, imported := range pkg.Imports { importPath := ImportPath(importPath) - // see note in gopls/internal/lsp/cache/load.go for an explanation of this check. + // see note in gopls/internal/cache/load.go for an explanation of this check. if importPath != "unsafe" && len(imported.CompiledGoFiles) == 0 { mp.DepsByImpPath[importPath] = "" // missing continue diff --git a/gopls/internal/lsp/cache/typerefs/refs.go b/gopls/internal/cache/typerefs/refs.go similarity index 99% rename from gopls/internal/lsp/cache/typerefs/refs.go rename to gopls/internal/cache/typerefs/refs.go index 53dcd5e4359..b389667ae7f 100644 --- a/gopls/internal/lsp/cache/typerefs/refs.go +++ b/gopls/internal/cache/typerefs/refs.go @@ -11,8 +11,8 @@ import ( "sort" "strings" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/frob" ) diff --git a/gopls/internal/lsp/cache/typerefs/refs_test.go b/gopls/internal/cache/typerefs/refs_test.go similarity index 97% rename from gopls/internal/lsp/cache/typerefs/refs_test.go rename to gopls/internal/cache/typerefs/refs_test.go index bbd0885f819..9bb9ec5bdfa 100644 --- a/gopls/internal/lsp/cache/typerefs/refs_test.go +++ b/gopls/internal/cache/typerefs/refs_test.go @@ -12,10 +12,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/cache/parsego" - "golang.org/x/tools/gopls/internal/lsp/cache/typerefs" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/typerefs" + "golang.org/x/tools/gopls/internal/protocol" ) // TestRefs checks that the analysis reports, for each exported member diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/cache/view.go similarity index 90% rename from gopls/internal/lsp/cache/view.go rename to gopls/internal/cache/view.go index bf2a8f045eb..ed52646f31d 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/cache/view.go @@ -2,7 +2,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package cache implements the caching layer for gopls. +// Package cache is the core of gopls: it is concerned with state +// management, dependency analysis, and invalidation; and it holds the +// machinery of type checking and modular static analysis. Its +// principal types are [Session], [Folder], [View], [Snapshot], +// [Cache], and [Package]. package cache import ( @@ -21,11 +25,9 @@ import ( "sync" "time" - "golang.org/x/mod/modfile" - "golang.org/x/mod/semver" + "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/maps" "golang.org/x/tools/gopls/internal/util/pathutil" @@ -121,6 +123,10 @@ type View struct { // initialization of snapshots. Do not change it without adjusting snapshot // accordingly. initializationSema chan struct{} + + // Document filters are constructed once, in View.filterFunc. + filterFuncOnce sync.Once + _filterFunc func(protocol.DocumentURI) bool // only accessed by View.filterFunc } // definition implements the viewDefiner interface. @@ -148,8 +154,10 @@ type viewDefinition struct { // workspaceModFiles holds the set of mod files active in this snapshot. // - // This is either empty, a single entry for the workspace go.mod file, or the - // set of mod files used by the workspace go.work file. + // For a go.work workspace, this is the set of workspace modfiles. For a + // go.mod workspace, this contains the go.mod file defining the workspace + // root, as well as any locally replaced modules (if + // "includeReplaceInWorkspace" is set). // // TODO(rfindley): should we just run `go list -m` to compute this set? workspaceModFiles map[protocol.DocumentURI]struct{} @@ -490,19 +498,26 @@ func (s *Snapshot) locateTemplateFiles(ctx context.Context) { // filterFunc returns a func that reports whether uri is filtered by the currently configured // directoryFilters. -// -// TODO(rfindley): memoize this func or filterer, as it is invariant on the -// view. func (v *View) filterFunc() func(protocol.DocumentURI) bool { - folderDir := v.folder.Dir.Path() - filterer := buildFilterer(folderDir, v.folder.Env.GOMODCACHE, v.folder.Options.DirectoryFilters) - return func(uri protocol.DocumentURI) bool { - // Only filter relative to the configured root directory. - if pathutil.InDir(folderDir, uri.Path()) { - return relPathExcludedByFilter(strings.TrimPrefix(uri.Path(), folderDir), filterer) + v.filterFuncOnce.Do(func() { + folderDir := v.folder.Dir.Path() + gomodcache := v.folder.Env.GOMODCACHE + var filters []string + filters = append(filters, v.folder.Options.DirectoryFilters...) + if pref := strings.TrimPrefix(gomodcache, folderDir); pref != gomodcache { + modcacheFilter := "-" + strings.TrimPrefix(filepath.ToSlash(pref), "/") + filters = append(filters, modcacheFilter) } - return false - } + filterer := NewFilterer(filters) + v._filterFunc = func(uri protocol.DocumentURI) bool { + // Only filter relative to the configured root directory. + if pathutil.InDir(folderDir, uri.Path()) { + return relPathExcludedByFilter(strings.TrimPrefix(uri.Path(), folderDir), filterer) + } + return false + } + }) + return v._filterFunc } // shutdown releases resources associated with the view. @@ -750,6 +765,7 @@ func (s *Snapshot) initialize(ctx context.Context, firstAttempt bool) { // By far the most common of these is a change to file state, but a query of // module upgrade information or vulnerabilities also affects gopls' behavior. type StateChange struct { + Modifications []file.Modification // if set, the raw modifications originating this change Files map[protocol.DocumentURI]file.Handle ModuleUpgrades map[protocol.DocumentURI]map[string]string Vulns map[protocol.DocumentURI]*vulncheck.Result @@ -925,6 +941,22 @@ func defineView(ctx context.Context, fs file.Source, folder *Folder, forFile fil // But gopls is less strict, allowing GOPATH mode if GO111MODULE="", and // AdHoc views if no module is found. + // gomodWorkspace is a helper to compute the correct set of workspace + // modfiles for a go.mod file, based on folder options. + gomodWorkspace := func() map[protocol.DocumentURI]unit { + modFiles := map[protocol.DocumentURI]struct{}{def.gomod: {}} + if folder.Options.IncludeReplaceInWorkspace { + includingReplace, err := goModModules(ctx, def.gomod, fs) + if err == nil { + modFiles = includingReplace + } else { + // If the go.mod file fails to parse, we don't know anything about + // replace directives, so fall back to a view of just the root module. + } + } + return modFiles + } + // Prefer a go.work file if it is available and contains the module relevant // to forURI. if def.adjustedGO111MODULE() != "off" && folder.Env.GOWORK != "off" && def.gowork != "" { @@ -945,7 +977,7 @@ func defineView(ctx context.Context, fs file.Source, folder *Folder, forFile fil if _, ok := def.workspaceModFiles[def.gomod]; !ok { def.typ = GoModView def.root = def.gomod.Dir() - def.workspaceModFiles = map[protocol.DocumentURI]unit{def.gomod: {}} + def.workspaceModFiles = gomodWorkspace() if def.envOverlay == nil { def.envOverlay = make(map[string]string) } @@ -964,7 +996,7 @@ func defineView(ctx context.Context, fs file.Source, folder *Folder, forFile fil if def.adjustedGO111MODULE() != "off" && def.gomod != "" { def.typ = GoModView def.root = def.gomod.Dir() - def.workspaceModFiles = map[protocol.DocumentURI]struct{}{def.gomod: {}} + def.workspaceModFiles = gomodWorkspace() return def, nil } @@ -1087,43 +1119,6 @@ func loadGoEnv(ctx context.Context, dir string, configEnv []string, runner *goco return nil } -// findWorkspaceModFile searches for a single go.mod file relative to the given -// folder URI, using the following algorithm: -// 1. if there is a go.mod file in a parent directory, return it -// 2. else, if there is exactly one nested module, return it -// 3. else, return "" -func findWorkspaceModFile(ctx context.Context, folderURI protocol.DocumentURI, fs file.Source, excludePath func(string) bool) (protocol.DocumentURI, error) { - match, err := findRootPattern(ctx, folderURI, "go.mod", fs) - if err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return "", ctxErr - } - return "", err - } - if match != "" { - return match, nil - } - - // ...else we should check if there's exactly one nested module. - all, err := findModules(folderURI, excludePath, 2) - if err == errExhausted { - // Fall-back behavior: if we don't find any modules after searching 10000 - // files, assume there are none. - event.Log(ctx, fmt.Sprintf("stopped searching for modules after %d files", fileLimit)) - return "", nil - } - if err != nil { - return "", err - } - if len(all) == 1 { - // range to access first element. - for uri := range all { - return uri, nil - } - } - return "", nil -} - // findRootPattern looks for files with the given basename in dir or any parent // directory of dir, using the provided FileSource. It returns the first match, // starting from dir and search parents. @@ -1286,47 +1281,6 @@ func globsMatchPath(globs, target string) bool { var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`) -// TODO(rstambler): Consolidate modURI and modContent back into a FileHandle -// after we have a version of the workspace go.mod file on disk. Getting a -// FileHandle from the cache for temporary files is problematic, since we -// cannot delete it. -// -// TODO(rfindley): move this to snapshot.go. -func (s *Snapshot) vendorEnabled(ctx context.Context, modURI protocol.DocumentURI, modContent []byte) (bool, error) { - // Legacy GOPATH workspace? - if len(s.view.workspaceModFiles) == 0 { - return false, nil - } - - // Explicit -mod flag? - matches := modFlagRegexp.FindStringSubmatch(s.view.folder.Env.GOFLAGS) - if len(matches) != 0 { - modFlag := matches[1] - if modFlag != "" { - // Don't override an explicit '-mod=vendor' argument. - // We do want to override '-mod=readonly': it would break various module code lenses, - // and on 1.16 we know -modfile is available, so we won't mess with go.mod anyway. - return modFlag == "vendor", nil - } - } - - modFile, err := modfile.Parse(modURI.Path(), modContent, nil) - if err != nil { - return false, err - } - - // No vendor directory? - // TODO(golang/go#57514): this is wrong if the working dir is not the module - // root. - if fi, err := os.Stat(filepath.Join(s.view.root.Path(), "vendor")); err != nil || !fi.IsDir() { - return false, nil - } - - // Vendoring enabled by default by go declaration in go.mod? - vendorEnabled := modFile.Go != nil && modFile.Go.Version != "" && semver.Compare("v"+modFile.Go.Version, "v1.14") >= 0 - return vendorEnabled, nil -} - // TODO(rfindley): clean up the redundancy of allFilesExcluded, // pathExcludedByFilterFunc, pathExcludedByFilter, view.filterFunc... func allFilesExcluded(files []string, filterFunc func(protocol.DocumentURI) bool) bool { @@ -1343,13 +1297,3 @@ func relPathExcludedByFilter(path string, filterer *Filterer) bool { path = strings.TrimPrefix(filepath.ToSlash(path), "/") return filterer.Disallow(path) } - -func buildFilterer(folder, gomodcache string, directoryFilters []string) *Filterer { - var filters []string - filters = append(filters, directoryFilters...) - if pref := strings.TrimPrefix(gomodcache, folder); pref != gomodcache { - modcacheFilter := "-" + strings.TrimPrefix(filepath.ToSlash(pref), "/") - filters = append(filters, modcacheFilter) - } - return NewFilterer(filters) -} diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/cache/view_test.go similarity index 75% rename from gopls/internal/lsp/cache/view_test.go rename to gopls/internal/cache/view_test.go index 44e0840a4e6..992a3d61828 100644 --- a/gopls/internal/lsp/cache/view_test.go +++ b/gopls/internal/cache/view_test.go @@ -4,13 +4,11 @@ package cache import ( - "context" "os" "path/filepath" "testing" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/test/integration/fake" + "golang.org/x/tools/gopls/internal/protocol" ) func TestCaseInsensitiveFilesystem(t *testing.T) { @@ -44,63 +42,6 @@ func TestCaseInsensitiveFilesystem(t *testing.T) { } } -func TestFindWorkspaceModFile(t *testing.T) { - workspace := ` --- a/go.mod -- -module a --- a/x/x.go -package x --- a/x/y/y.go -package x --- b/go.mod -- -module b --- b/c/go.mod -- -module bc --- d/gopls.mod -- -module d-goplsworkspace --- d/e/go.mod -- -module de --- f/g/go.mod -- -module fg -` - dir, err := fake.Tempdir(fake.UnpackTxt(workspace)) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - - tests := []struct { - folder, want string - }{ - {"", ""}, // no module at root, and more than one nested module - {"a", "a/go.mod"}, - {"a/x", "a/go.mod"}, - {"a/x/y", "a/go.mod"}, - {"b/c", "b/c/go.mod"}, - {"d", "d/e/go.mod"}, - {"d/e", "d/e/go.mod"}, - {"f", "f/g/go.mod"}, - } - - for _, test := range tests { - ctx := context.Background() - rel := fake.RelativeTo(dir) - folderURI := protocol.URIFromPath(rel.AbsPath(test.folder)) - excludeNothing := func(string) bool { return false } - got, err := findWorkspaceModFile(ctx, folderURI, New(nil), excludeNothing) - if err != nil { - t.Fatal(err) - } - want := protocol.DocumentURI("") - if test.want != "" { - want = protocol.URIFromPath(rel.AbsPath(test.want)) - } - if got != want { - t.Errorf("findWorkspaceModFile(%q) = %q, want %q", test.folder, got, want) - } - } -} - func TestInVendor(t *testing.T) { for _, tt := range []struct { path string diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/cache/workspace.go similarity index 53% rename from gopls/internal/lsp/cache/workspace.go rename to gopls/internal/cache/workspace.go index 9e54289d2f7..07134b3da00 100644 --- a/gopls/internal/lsp/cache/workspace.go +++ b/gopls/internal/cache/workspace.go @@ -8,17 +8,17 @@ import ( "context" "errors" "fmt" - "io/fs" "path/filepath" - "strings" "golang.org/x/mod/modfile" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" ) -// TODO(rfindley): now that experimentalWorkspaceModule is gone, this file can -// be massively cleaned up and/or removed. +// isGoWork reports if uri is a go.work file. +func isGoWork(uri protocol.DocumentURI) bool { + return filepath.Base(uri.Path()) == "go.work" +} // goWorkModules returns the URIs of go.mod files named by the go.work file. func goWorkModules(ctx context.Context, gowork protocol.DocumentURI, fs file.Source) (map[protocol.DocumentURI]unit, error) { @@ -36,16 +36,28 @@ func goWorkModules(ctx context.Context, gowork protocol.DocumentURI, fs file.Sou if err != nil { return nil, fmt.Errorf("parsing go.work: %w", err) } - modFiles := make(map[protocol.DocumentURI]unit) + var usedDirs []string for _, use := range workFile.Use { - modDir := filepath.FromSlash(use.Path) + usedDirs = append(usedDirs, use.Path) + } + return localModFiles(dir, usedDirs), nil +} + +// localModFiles builds a set of local go.mod files referenced by +// goWorkOrModPaths, which is a slice of paths as contained in a go.work 'use' +// directive or go.mod 'replace' directive (and which therefore may use either +// '/' or '\' as a path separator). +func localModFiles(relativeTo string, goWorkOrModPaths []string) map[protocol.DocumentURI]unit { + modFiles := make(map[protocol.DocumentURI]unit) + for _, path := range goWorkOrModPaths { + modDir := filepath.FromSlash(path) if !filepath.IsAbs(modDir) { - modDir = filepath.Join(dir, modDir) + modDir = filepath.Join(relativeTo, modDir) } modURI := protocol.URIFromPath(filepath.Join(modDir, "go.mod")) modFiles[modURI] = unit{} } - return modFiles, nil + return modFiles } // isGoMod reports if uri is a go.mod file. @@ -53,9 +65,33 @@ func isGoMod(uri protocol.DocumentURI) bool { return filepath.Base(uri.Path()) == "go.mod" } -// isGoWork reports if uri is a go.work file. -func isGoWork(uri protocol.DocumentURI) bool { - return filepath.Base(uri.Path()) == "go.work" +// goModModules returns the URIs of "workspace" go.mod files defined by a +// go.mod file. This set is defined to be the given go.mod file itself, as well +// as the modfiles of any locally replaced modules in the go.mod file. +func goModModules(ctx context.Context, gomod protocol.DocumentURI, fs file.Source) (map[protocol.DocumentURI]unit, error) { + fh, err := fs.ReadFile(ctx, gomod) + if err != nil { + return nil, err // canceled + } + content, err := fh.Content() + if err != nil { + return nil, err + } + filename := gomod.Path() + dir := filepath.Dir(filename) + modFile, err := modfile.Parse(filename, content, nil) + if err != nil { + return nil, err + } + var localReplaces []string + for _, replace := range modFile.Replace { + if modfile.IsDirectoryPath(replace.New.Path) { + localReplaces = append(localReplaces, replace.New.Path) + } + } + modFiles := localModFiles(dir, localReplaces) + modFiles[gomod] = unit{} + return modFiles, nil } // fileExists reports whether the file has a Content (which may be empty). @@ -74,50 +110,3 @@ var errExhausted = errors.New("exhausted") // Note: per golang/go#56496, the previous limit of 1M files was too slow, at // which point this limit was decreased to 100K. const fileLimit = 100_000 - -// findModules recursively walks the root directory looking for go.mod files, -// returning the set of modules it discovers. If modLimit is non-zero, -// searching stops once modLimit modules have been found. -// -// TODO(rfindley): consider overlays. -func findModules(root protocol.DocumentURI, excludePath func(string) bool, modLimit int) (map[protocol.DocumentURI]struct{}, error) { - // Walk the view's folder to find all modules in the view. - modFiles := make(map[protocol.DocumentURI]struct{}) - searched := 0 - errDone := errors.New("done") - err := filepath.WalkDir(root.Path(), func(path string, info fs.DirEntry, err error) error { - if err != nil { - // Probably a permission error. Keep looking. - return filepath.SkipDir - } - // For any path that is not the workspace folder, check if the path - // would be ignored by the go command. Vendor directories also do not - // contain workspace modules. - if info.IsDir() && path != root.Path() { - suffix := strings.TrimPrefix(path, root.Path()) - switch { - case checkIgnored(suffix), - strings.Contains(filepath.ToSlash(suffix), "/vendor/"), - excludePath(suffix): - return filepath.SkipDir - } - } - // We're only interested in go.mod files. - uri := protocol.URIFromPath(path) - if isGoMod(uri) { - modFiles[uri] = struct{}{} - } - if modLimit > 0 && len(modFiles) >= modLimit { - return errDone - } - searched++ - if fileLimit > 0 && searched >= fileLimit { - return errExhausted - } - return nil - }) - if err == errDone { - return modFiles, nil - } - return modFiles, err -} diff --git a/gopls/internal/lsp/cache/xrefs/xrefs.go b/gopls/internal/cache/xrefs/xrefs.go similarity index 97% rename from gopls/internal/lsp/cache/xrefs/xrefs.go rename to gopls/internal/cache/xrefs/xrefs.go index c6a5f04bc73..6ab54329d38 100644 --- a/gopls/internal/lsp/cache/xrefs/xrefs.go +++ b/gopls/internal/cache/xrefs/xrefs.go @@ -14,9 +14,9 @@ import ( "sort" "golang.org/x/tools/go/types/objectpath" - "golang.org/x/tools/gopls/internal/lsp/cache/metadata" - "golang.org/x/tools/gopls/internal/lsp/cache/parsego" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/frob" "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/typeparams" diff --git a/gopls/internal/cmd/call_hierarchy.go b/gopls/internal/cmd/call_hierarchy.go index 5566bdcadf9..82c18d0d28f 100644 --- a/gopls/internal/cmd/call_hierarchy.go +++ b/gopls/internal/cmd/call_hierarchy.go @@ -10,7 +10,7 @@ import ( "fmt" "strings" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/capabilities_test.go b/gopls/internal/cmd/capabilities_test.go index 9a66e6dd2d1..47670572285 100644 --- a/gopls/internal/cmd/capabilities_test.go +++ b/gopls/internal/cmd/capabilities_test.go @@ -11,8 +11,8 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/server" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/internal/testenv" @@ -40,10 +40,10 @@ func TestCapabilities(t *testing.T) { } defer os.RemoveAll(tmpDir) - app := New("gopls-test", tmpDir, os.Environ(), nil) + app := New(nil) params := &protocol.ParamInitialize{} - params.RootURI = protocol.URIFromPath(app.wd) + params.RootURI = protocol.URIFromPath(tmpDir) params.Capabilities.Workspace.Configuration = true // Send an initialize request to the server. diff --git a/gopls/internal/cmd/check.go b/gopls/internal/cmd/check.go index 4d0ff046dbb..2d7a7674226 100644 --- a/gopls/internal/cmd/check.go +++ b/gopls/internal/cmd/check.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" ) // check implements the check verb for gopls. diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index 3c9e8f9d71a..31ca0981c87 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go @@ -21,12 +21,12 @@ import ( "text/tabwriter" "time" + "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/filecache" - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/lsprpc" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsprpc" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/server" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/browser" @@ -35,7 +35,6 @@ import ( "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/tool" - "golang.org/x/tools/internal/xcontext" ) // Application is the main application as passed to tool.Main @@ -54,15 +53,6 @@ type Application struct { // the options configuring function to invoke when building a server options func(*settings.Options) - // The name of the binary, used in help and telemetry. - name string - - // The working directory to run commands in. - wd string - - // The environment variables to use. - env []string - // Support for remote LSP server. Remote string `flag:"remote" help:"forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. 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."` @@ -105,15 +95,9 @@ func (app *Application) verbose() bool { } // New returns a new Application ready to run. -func New(name, wd string, env []string, options func(*settings.Options)) *Application { - if wd == "" { - wd, _ = os.Getwd() - } +func New(options func(*settings.Options)) *Application { app := &Application{ options: options, - name: name, - wd: wd, - env: env, OCAgent: "off", //TODO: Remove this line to default the exporter to on Serve: Serve{ @@ -125,7 +109,7 @@ func New(name, wd string, env []string, options func(*settings.Options)) *Applic } // Name implements tool.Application returning the binary name. -func (app *Application) Name() string { return app.name } +func (app *Application) Name() string { return "gopls" } // Usage implements tool.Application returning empty extra argument usage. func (app *Application) Usage() string { return "" } @@ -250,7 +234,7 @@ func (app *Application) Run(ctx context.Context, args ...string) error { // executable, and immediately runs a gc. filecache.Start() - ctx = debug.WithInstance(ctx, app.wd, app.OCAgent) + ctx = debug.WithInstance(ctx, app.OCAgent) if len(args) == 0 { s := flag.NewFlagSet(app.Name(), flag.ExitOnError) return tool.Run(ctx, s, &app.Serve, args) @@ -341,22 +325,6 @@ func (app *Application) connect(ctx context.Context, onProgress func(*protocol.P } return conn, nil - case strings.HasPrefix(app.Remote, "internal@"): - internalMu.Lock() - defer internalMu.Unlock() - opts := settings.DefaultOptions(app.options) - key := fmt.Sprintf("%s %v %v %v", app.wd, opts.PreferredContentFormat, opts.HierarchicalDocumentSymbolSupport, opts.SymbolMatcher) - if c := internalConnections[key]; c != nil { - return c, nil - } - remote := app.Remote[len("internal@"):] - ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server - connection, err := app.connectRemote(ctx, remote) - if err != nil { - return nil, err - } - internalConnections[key] = connection - return connection, nil default: return app.connectRemote(ctx, app.Remote) } @@ -380,8 +348,12 @@ func (app *Application) connectRemote(ctx context.Context, remote string) (*conn } func (c *connection) initialize(ctx context.Context, options func(*settings.Options)) error { + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("finding workdir: %v", err) + } params := &protocol.ParamInitialize{} - params.RootURI = protocol.URIFromPath(c.client.app.wd) + params.RootURI = protocol.URIFromPath(wd) params.Capabilities.Workspace.Configuration = true // Make sure to respect configured options when sending initialize request. @@ -513,16 +485,7 @@ func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfigur if item.Section != "gopls" { continue } - env := map[string]interface{}{} - for _, value := range c.app.env { - l := strings.SplitN(value, "=", 2) - if len(l) != 2 { - continue - } - env[l[0]] = l[1] - } m := map[string]interface{}{ - "env": env, "analyses": map[string]any{ "fillreturns": true, "nonewvars": true, @@ -776,10 +739,6 @@ func (c *connection) diagnoseFiles(ctx context.Context, files []protocol.Documen } func (c *connection) terminate(ctx context.Context) { - if strings.HasPrefix(c.client.app.Remote, "internal@") { - // internal connections need to be left alive for the next test - return - } //TODO: do we need to handle errors on these calls? c.Shutdown(ctx) //TODO: right now calling exit terminates the process, we should rethink that diff --git a/gopls/internal/cmd/codelens.go b/gopls/internal/cmd/codelens.go index f861eca1b5e..28986cc6bbb 100644 --- a/gopls/internal/cmd/codelens.go +++ b/gopls/internal/cmd/codelens.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/internal/tool" ) @@ -71,7 +71,7 @@ func (r *codelens) Run(ctx context.Context, args ...string) error { // Override the default setting for codelenses[Test], which is // off by default because VS Code has a superior client-side // implementation. But this client is not VS Code. - // See source.LensFuncs(). + // See golang.LensFuncs(). origOptions := r.app.options r.app.options = func(opts *settings.Options) { origOptions(opts) diff --git a/gopls/internal/cmd/definition.go b/gopls/internal/cmd/definition.go index c20dcc14ffa..e5e119b8da8 100644 --- a/gopls/internal/cmd/definition.go +++ b/gopls/internal/cmd/definition.go @@ -12,7 +12,7 @@ import ( "os" "strings" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/execute.go b/gopls/internal/cmd/execute.go index 22e7820b36b..381c2a7aa95 100644 --- a/gopls/internal/cmd/execute.go +++ b/gopls/internal/cmd/execute.go @@ -13,8 +13,8 @@ import ( "os" "strings" - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/server" "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/tool" diff --git a/gopls/internal/cmd/folding_range.go b/gopls/internal/cmd/folding_range.go index 401224156ba..13f78c197a5 100644 --- a/gopls/internal/cmd/folding_range.go +++ b/gopls/internal/cmd/folding_range.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/format.go b/gopls/internal/cmd/format.go index 9643cd2cf73..75982c9efba 100644 --- a/gopls/internal/cmd/format.go +++ b/gopls/internal/cmd/format.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" ) // format implements the format verb for gopls. diff --git a/gopls/internal/cmd/help_test.go b/gopls/internal/cmd/help_test.go index 5c11cd5163d..dd79c2f7e02 100644 --- a/gopls/internal/cmd/help_test.go +++ b/gopls/internal/cmd/help_test.go @@ -32,7 +32,7 @@ const appName = "gopls" func TestHelpFiles(t *testing.T) { testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. - app := cmd.New(appName, "", nil, nil) + app := cmd.New(nil) ctx := context.Background() for _, page := range append(app.Commands(), app) { t.Run(page.Name(), func(t *testing.T) { @@ -65,7 +65,7 @@ func TestHelpFiles(t *testing.T) { func TestVerboseHelp(t *testing.T) { testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. - app := cmd.New(appName, "", nil, nil) + app := cmd.New(nil) ctx := context.Background() var buf bytes.Buffer s := flag.NewFlagSet(appName, flag.ContinueOnError) diff --git a/gopls/internal/cmd/highlight.go b/gopls/internal/cmd/highlight.go index 3fddf9d6dbe..9c1488b30be 100644 --- a/gopls/internal/cmd/highlight.go +++ b/gopls/internal/cmd/highlight.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/implementation.go b/gopls/internal/cmd/implementation.go index 180d04dff19..fcfb63185b4 100644 --- a/gopls/internal/cmd/implementation.go +++ b/gopls/internal/cmd/implementation.go @@ -10,7 +10,7 @@ import ( "fmt" "sort" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/imports.go b/gopls/internal/cmd/imports.go index d32b0941646..414ce3473b0 100644 --- a/gopls/internal/cmd/imports.go +++ b/gopls/internal/cmd/imports.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index 4da649f5b4c..aabb8c223b9 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go @@ -41,8 +41,9 @@ import ( "golang.org/x/tools/gopls/internal/cmd" "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" + "golang.org/x/tools/gopls/internal/version" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/tool" "golang.org/x/tools/txtar" @@ -55,7 +56,7 @@ func TestVersion(t *testing.T) { tree := writeTree(t, "") // There's not much we can robustly assert about the actual version. - want := debug.Version() // e.g. "master" + want := version.Version() // e.g. "master" // basic { @@ -64,6 +65,13 @@ func TestVersion(t *testing.T) { res.checkStdout(want) } + // basic, with version override + { + res := goplsWithEnv(t, tree, []string{"TEST_GOPLS_VERSION=v1.2.3"}, "version") + res.checkExit(true) + res.checkStdout(`v1\.2\.3`) + } + // -json flag { res := gopls(t, tree, "version", "-json") @@ -894,13 +902,13 @@ package foo if got := len(stats2.BugReports); got > 0 { t.Errorf("Got %d bug reports with -anon, want 0. Reports:%+v", got, stats2.BugReports) } - var stats2AsMap map[string]interface{} + var stats2AsMap map[string]any if err := json.Unmarshal([]byte(res2.stdout), &stats2AsMap); err != nil { t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) } // GOPACKAGESDRIVER is user information, but is ok to print zero value. - if v, ok := stats2AsMap["GOPACKAGESDRIVER"]; !ok || v != "" { - t.Errorf(`Got GOPACKAGESDRIVER=(%q, %v); want ("", true(found))`, v, ok) + if v, ok := stats2AsMap["GOPACKAGESDRIVER"]; ok && v != "" { + t.Errorf(`Got GOPACKAGESDRIVER=(%v, %v); want ("", true(found))`, v, ok) } } @@ -1034,7 +1042,11 @@ func goplsMain() { bug.PanicOnBugs = true } - tool.Main(context.Background(), cmd.New("gopls", "", nil, hooks.Options), os.Args[1:]) + if v := os.Getenv("TEST_GOPLS_VERSION"); v != "" { + version.VersionOverride = v + } + + tool.Main(context.Background(), cmd.New(hooks.Options), os.Args[1:]) } // writeTree extracts a txtar archive into a new directory and returns its path. @@ -1077,6 +1089,7 @@ func goplsWithEnv(t *testing.T, dir string, env []string, args ...string) *resul goplsCmd := exec.Command(os.Args[0], args...) goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain") + goplsCmd.Env = append(goplsCmd.Env, "GOPACKAGESDRIVER=off") goplsCmd.Env = append(goplsCmd.Env, env...) goplsCmd.Dir = dir goplsCmd.Stdout = new(bytes.Buffer) diff --git a/gopls/internal/cmd/links.go b/gopls/internal/cmd/links.go index f7041ecaa08..0f1d671a503 100644 --- a/gopls/internal/cmd/links.go +++ b/gopls/internal/cmd/links.go @@ -11,7 +11,7 @@ import ( "fmt" "os" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/parsespan.go b/gopls/internal/cmd/parsespan.go index c70b85e3432..556beb9730e 100644 --- a/gopls/internal/cmd/parsespan.go +++ b/gopls/internal/cmd/parsespan.go @@ -9,7 +9,7 @@ import ( "strings" "unicode/utf8" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" ) // parseSpan returns the location represented by the input. diff --git a/gopls/internal/cmd/prepare_rename.go b/gopls/internal/cmd/prepare_rename.go index 69ed6cab262..c7901e6484d 100644 --- a/gopls/internal/cmd/prepare_rename.go +++ b/gopls/internal/cmd/prepare_rename.go @@ -10,7 +10,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/references.go b/gopls/internal/cmd/references.go index 670f5fe01d8..3c294c71b14 100644 --- a/gopls/internal/cmd/references.go +++ b/gopls/internal/cmd/references.go @@ -10,7 +10,7 @@ import ( "fmt" "sort" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/remote.go b/gopls/internal/cmd/remote.go index 684981cfff8..8de4365fc9d 100644 --- a/gopls/internal/cmd/remote.go +++ b/gopls/internal/cmd/remote.go @@ -13,8 +13,8 @@ import ( "log" "os" - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsprpc" + "golang.org/x/tools/gopls/internal/protocol/command" ) type remote struct { diff --git a/gopls/internal/cmd/rename.go b/gopls/internal/cmd/rename.go index 589f202c8c5..6d831681c19 100644 --- a/gopls/internal/cmd/rename.go +++ b/gopls/internal/cmd/rename.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/semantictokens.go b/gopls/internal/cmd/semantictokens.go index 538cedc6c62..f181f30c420 100644 --- a/gopls/internal/cmd/semantictokens.go +++ b/gopls/internal/cmd/semantictokens.go @@ -13,7 +13,7 @@ import ( "os" "unicode/utf8" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" ) diff --git a/gopls/internal/cmd/serve.go b/gopls/internal/cmd/serve.go index 03eb4520ff0..a2f9be9f7e2 100644 --- a/gopls/internal/cmd/serve.go +++ b/gopls/internal/cmd/serve.go @@ -14,10 +14,10 @@ import ( "os" "time" + "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug" - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/lsprpc" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsprpc" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/telemetry" "golang.org/x/tools/internal/fakenet" "golang.org/x/tools/internal/jsonrpc2" diff --git a/gopls/internal/cmd/signature.go b/gopls/internal/cmd/signature.go index 240266942f8..cf976a64859 100644 --- a/gopls/internal/cmd/signature.go +++ b/gopls/internal/cmd/signature.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/span.go b/gopls/internal/cmd/span.go index fe8f9477321..4753d534350 100644 --- a/gopls/internal/cmd/span.go +++ b/gopls/internal/cmd/span.go @@ -13,7 +13,7 @@ import ( "sort" "strings" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" ) // A span represents a range of text within a source file. The start @@ -28,7 +28,7 @@ import ( // representations, such as go/token (also UTF-8) or the LSP protocol // (UTF-16). The latter requires access to file contents. // -// See overview comments at ../lsp/protocol/mapper.go. +// See overview comments at ../protocol/mapper.go. type span struct { v _span } diff --git a/gopls/internal/cmd/stats.go b/gopls/internal/cmd/stats.go index 9417bfe9ff0..8da1a1a6ae8 100644 --- a/gopls/internal/cmd/stats.go +++ b/gopls/internal/cmd/stats.go @@ -19,13 +19,13 @@ import ( "sync" "time" - "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/filecache" - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/server" "golang.org/x/tools/gopls/internal/settings" - goplsbug "golang.org/x/tools/gopls/internal/util/bug" + bugpkg "golang.org/x/tools/gopls/internal/util/bug" + versionpkg "golang.org/x/tools/gopls/internal/version" "golang.org/x/tools/internal/event" ) @@ -74,7 +74,7 @@ func (s *stats) Run(ctx context.Context, args ...string) error { GOARCH: runtime.GOARCH, GOPLSCACHE: os.Getenv("GOPLSCACHE"), GoVersion: runtime.Version(), - GoplsVersion: debug.Version(), + GoplsVersion: versionpkg.Version(), GOPACKAGESDRIVER: os.Getenv("GOPACKAGESDRIVER"), } @@ -147,7 +147,7 @@ func (s *stats) Run(ctx context.Context, args ...string) error { do("Gathering bug reports", func() error { stats.CacheDir, stats.BugReports = filecache.BugReports() if stats.BugReports == nil { - stats.BugReports = []goplsbug.Bug{} // non-nil for JSON + stats.BugReports = []bugpkg.Bug{} // non-nil for JSON } return nil }) @@ -180,7 +180,7 @@ func (s *stats) Run(ctx context.Context, args ...string) error { if _, err := do("Collecting directory info", func() error { var err error - stats.DirStats, err = findDirStats(ctx) + stats.DirStats, err = findDirStats() if err != nil { return err } @@ -232,7 +232,7 @@ type GoplsStats struct { GOPACKAGESDRIVER string InitialWorkspaceLoadDuration string `anon:"ok"` // in time.Duration string form CacheDir string - BugReports []goplsbug.Bug + BugReports []bugpkg.Bug MemStats command.MemStatsResult `anon:"ok"` WorkspaceStats command.WorkspaceStatsResult `anon:"ok"` DirStats dirStats `anon:"ok"` @@ -248,7 +248,7 @@ type dirStats struct { // findDirStats collects information about the current directory and its // subdirectories. -func findDirStats(ctx context.Context) (dirStats, error) { +func findDirStats() (dirStats, error) { var ds dirStats filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { if err != nil { diff --git a/gopls/internal/cmd/suggested_fix.go b/gopls/internal/cmd/suggested_fix.go index 9fe64977e7d..f6a88be91ce 100644 --- a/gopls/internal/cmd/suggested_fix.go +++ b/gopls/internal/cmd/suggested_fix.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/symbols.go b/gopls/internal/cmd/symbols.go index 5000046f66f..249397d320f 100644 --- a/gopls/internal/cmd/symbols.go +++ b/gopls/internal/cmd/symbols.go @@ -11,7 +11,7 @@ import ( "fmt" "sort" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/usage/vulncheck.hlp b/gopls/internal/cmd/usage/vulncheck.hlp index d16cb130871..7f2818dd40c 100644 --- a/gopls/internal/cmd/usage/vulncheck.hlp +++ b/gopls/internal/cmd/usage/vulncheck.hlp @@ -6,7 +6,7 @@ Usage: WARNING: this command is for internal-use only. By default, the command outputs a JSON-encoded - golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult + golang.org/x/tools/gopls/internal/protocol/command.VulncheckResult message. Example: $ gopls vulncheck diff --git a/gopls/internal/cmd/vulncheck.go b/gopls/internal/cmd/vulncheck.go index 855b9eef830..7babf0d14d7 100644 --- a/gopls/internal/cmd/vulncheck.go +++ b/gopls/internal/cmd/vulncheck.go @@ -30,7 +30,7 @@ func (v *vulncheck) DetailedHelp(f *flag.FlagSet) { WARNING: this command is for internal-use only. By default, the command outputs a JSON-encoded - golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult + golang.org/x/tools/gopls/internal/protocol/command.VulncheckResult message. Example: $ gopls vulncheck diff --git a/gopls/internal/cmd/workspace_symbol.go b/gopls/internal/cmd/workspace_symbol.go index f41a85f6466..9fa7526a24d 100644 --- a/gopls/internal/cmd/workspace_symbol.go +++ b/gopls/internal/cmd/workspace_symbol.go @@ -10,7 +10,7 @@ import ( "fmt" "strings" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/debug/info.go b/gopls/internal/debug/info.go index 84027ec43e1..b2824d86f38 100644 --- a/gopls/internal/debug/info.go +++ b/gopls/internal/debug/info.go @@ -14,6 +14,8 @@ import ( "runtime" "runtime/debug" "strings" + + "golang.org/x/tools/gopls/internal/version" ) type PrintMode int @@ -25,16 +27,6 @@ const ( JSON ) -// Version is a manually-updated mechanism for tracking versions. -func Version() string { - if info, ok := debug.ReadBuildInfo(); ok { - if info.Main.Version != "" { - return info.Main.Version - } - } - return "(unknown)" -} - // ServerVersion is the format used by gopls to report its version to the // client. This format is structured so that the client can parse it easily. type ServerVersion struct { @@ -48,12 +40,12 @@ type ServerVersion struct { func VersionInfo() *ServerVersion { if info, ok := debug.ReadBuildInfo(); ok { return &ServerVersion{ - Version: Version(), + Version: version.Version(), BuildInfo: info, } } return &ServerVersion{ - Version: Version(), + Version: version.Version(), BuildInfo: &debug.BuildInfo{ Path: "gopls, built in GOPATH mode", GoVersion: runtime.Version(), @@ -63,11 +55,12 @@ func VersionInfo() *ServerVersion { // PrintServerInfo writes HTML debug info to w for the Instance. func (i *Instance) PrintServerInfo(ctx context.Context, w io.Writer) { + workDir, _ := os.Getwd() section(w, HTML, "Server Instance", func() { fmt.Fprintf(w, "Start time: %v\n", i.StartTime) fmt.Fprintf(w, "LogFile: %s\n", i.Logfile) fmt.Fprintf(w, "pid: %d\n", os.Getpid()) - fmt.Fprintf(w, "Working directory: %s\n", i.Workdir) + fmt.Fprintf(w, "Working directory: %s\n", workDir) fmt.Fprintf(w, "Address: %s\n", i.ServerAddress) fmt.Fprintf(w, "Debug address: %s\n", i.DebugAddress()) }) @@ -123,11 +116,11 @@ func section(w io.Writer, mode PrintMode, title string, body func()) { } func printBuildInfo(w io.Writer, info *ServerVersion, verbose bool, mode PrintMode) { - fmt.Fprintf(w, "%v %v\n", info.Path, Version()) - printModuleInfo(w, info.Main, mode) + fmt.Fprintf(w, "%v %v\n", info.Path, version.Version()) if !verbose { return } + printModuleInfo(w, info.Main, mode) for _, dep := range info.Deps { printModuleInfo(w, *dep, mode) } diff --git a/gopls/internal/debug/info_test.go b/gopls/internal/debug/info_test.go index 3bc9290c157..7f24b696682 100644 --- a/gopls/internal/debug/info_test.go +++ b/gopls/internal/debug/info_test.go @@ -11,6 +11,8 @@ import ( "encoding/json" "runtime" "testing" + + "golang.org/x/tools/gopls/internal/version" ) func TestPrintVersionInfoJSON(t *testing.T) { @@ -27,7 +29,7 @@ func TestPrintVersionInfoJSON(t *testing.T) { if g, w := got.GoVersion, runtime.Version(); g != w { t.Errorf("go version = %v, want %v", g, w) } - if g, w := got.Version, Version(); g != w { + if g, w := got.Version, version.Version(); g != w { t.Errorf("gopls version = %v, want %v", g, w) } // Other fields of BuildInfo may not be available during test. @@ -41,7 +43,7 @@ func TestPrintVersionInfoPlainText(t *testing.T) { res := buf.Bytes() // Other fields of BuildInfo may not be available during test. - wantGoplsVersion, wantGoVersion := Version(), runtime.Version() + wantGoplsVersion, wantGoVersion := version.Version(), runtime.Version() if !bytes.Contains(res, []byte(wantGoplsVersion)) || !bytes.Contains(res, []byte(wantGoVersion)) { t.Errorf("plaintext output = %q,\nwant (version: %v, go: %v)", res, wantGoplsVersion, wantGoVersion) } diff --git a/gopls/internal/debug/rpc.go b/gopls/internal/debug/rpc.go index 5610021479c..0fee0f4a435 100644 --- a/gopls/internal/debug/rpc.go +++ b/gopls/internal/debug/rpc.go @@ -84,19 +84,19 @@ func (r *Rpcs) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) co defer r.mu.Unlock() switch { case event.IsStart(ev): - if _, stats := r.getRPCSpan(ctx, ev); stats != nil { + if _, stats := r.getRPCSpan(ctx); stats != nil { stats.Started++ } case event.IsEnd(ev): - span, stats := r.getRPCSpan(ctx, ev) + span, stats := r.getRPCSpan(ctx) if stats != nil { - endRPC(ctx, ev, span, stats) + endRPC(span, stats) } case event.IsMetric(ev): sent := byteUnits(tag.SentBytes.Get(lm)) rec := byteUnits(tag.ReceivedBytes.Get(lm)) if sent != 0 || rec != 0 { - if _, stats := r.getRPCSpan(ctx, ev); stats != nil { + if _, stats := r.getRPCSpan(ctx); stats != nil { stats.Sent += sent stats.Received += rec } @@ -105,7 +105,7 @@ func (r *Rpcs) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) co return ctx } -func endRPC(ctx context.Context, ev core.Event, span *export.Span, stats *rpcStats) { +func endRPC(span *export.Span, stats *rpcStats) { // update the basic counts stats.Completed++ @@ -152,7 +152,7 @@ func endRPC(ctx context.Context, ev core.Event, span *export.Span, stats *rpcSta } } -func (r *Rpcs) getRPCSpan(ctx context.Context, ev core.Event) (*export.Span, *rpcStats) { +func (r *Rpcs) getRPCSpan(ctx context.Context) (*export.Span, *rpcStats) { // get the span span := export.GetSpan(ctx) if span == nil { diff --git a/gopls/internal/debug/serve.go b/gopls/internal/debug/serve.go index d7ba381d3d5..62e416829fe 100644 --- a/gopls/internal/debug/serve.go +++ b/gopls/internal/debug/serve.go @@ -24,9 +24,9 @@ import ( "sync" "time" + "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug/log" - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/core" @@ -51,7 +51,6 @@ type Instance struct { Logfile string StartTime time.Time ServerAddress string - Workdir string OCAgentConfig string LogWriter io.Writer @@ -368,10 +367,9 @@ func GetInstance(ctx context.Context) *Instance { // WithInstance creates debug instance ready for use using the supplied // configuration and stores it in the returned context. -func WithInstance(ctx context.Context, workdir, agent string) context.Context { +func WithInstance(ctx context.Context, agent string) context.Context { i := &Instance{ StartTime: time.Now(), - Workdir: workdir, OCAgentConfig: agent, } i.LogWriter = os.Stderr @@ -464,7 +462,6 @@ func (i *Instance) Serve(ctx context.Context, addr string) (string, error) { mux.HandleFunc("/analysis/", render(AnalysisTmpl, i.getAnalysis)) 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)) @@ -646,12 +643,17 @@ var BaseTemplate = template.Must(template.New("").Parse(` width:6rem; } td.value { - text-align: right; + text-align: right; } ul.spans { font-family: monospace; font-size: 85%; } +body { + font-family: sans-serif; + font-size: 1rem; + line-height: normal; +} {{block "head" .}}{{end}} @@ -676,7 +678,6 @@ Unknown page {{define "clientlink"}}Client {{.}}{{end}} {{define "serverlink"}}Server {{.}}{{end}} {{define "sessionlink"}}Session {{.}}{{end}} -{{define "viewlink"}}View {{.}}{{end}} `)).Funcs(template.FuncMap{ "fuint64": fuint64, "fuint32": fuint32, @@ -708,7 +709,7 @@ Unknown page }) var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` -{{define "title"}}GoPls server information{{end}} +{{define "title"}}Gopls server information{{end}} {{define "body"}}

Caches

@@ -724,7 +725,7 @@ var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` `)) var InfoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` -{{define "title"}}GoPls version information{{end}} +{{define "title"}}Gopls version information{{end}} {{define "body"}} {{.}} {{end}} @@ -773,6 +774,13 @@ var CacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` {{define "body"}}

memoize.Store entries

+

File stats

+

+{{- $stats := .FileStats -}} +Total: {{$stats.Total}}
+Largest: {{$stats.Largest}}
+Errors: {{$stats.Errs}}
+

{{end}} `)) @@ -808,7 +816,16 @@ var SessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` {{define "body"}} From: {{template "cachelink" .Cache.ID}}

Views

- +

Overlays

{{$session := .}}