Skip to content

Commit db5406b

Browse files
committed
gopls/internal/lsp: don't recompute diagnostics during code actions
As noted in an outstanding TODO, there's no reason to re-compute diagnostics during code actions. This may have been cheap in the previous gopls architecture, but still complicated the code. With the new architecture it has significant cost, especially when running analysis with a different set of analyzers. Fix this by instead unbundling serialized code actions, or failing that by matching code action context with previously published diagnostics stored on the server. After doing so, what remained was the additional "diagnostics" produced by convenience analyzers. These are (rather pragmatically) refactored to be computed from typed syntax alone, outside of the analysis framework. Incidentally, this fixes the long-standing golang/go#51478. The analyzers are left as thin shells, so that existing analyzer configuration will continue to work for the short term. Longer term, we should design a new mechanism for parameterizing refactorings and deprecate the old analyzer settings. Make some additional cleanup along the way, and some other relatively superficial changes to get tests to pass. Fixes golang/go#51478 Fixes golang/go#61508 Change-Id: I0218d4fc15a6fdb5f2fd5c0560b9d9afa079b84d Reviewed-on: https://go-review.googlesource.com/c/tools/+/511995 gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]>
1 parent 4810eda commit db5406b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+492
-446
lines changed

gopls/internal/lsp/analysis/fillstruct/fillstruct.go

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,26 +48,34 @@ var Analyzer = &analysis.Analyzer{
4848
RunDespiteErrors: true,
4949
}
5050

51+
// TODO(rfindley): remove this thin wrapper around the fillstruct refactoring,
52+
// and eliminate the fillstruct analyzer.
53+
//
54+
// Previous iterations used the analysis framework for computing refactorings,
55+
// which proved inefficient.
5156
func run(pass *analysis.Pass) (interface{}, error) {
5257
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
58+
for _, d := range DiagnoseFillableStructs(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) {
59+
pass.Report(d)
60+
}
61+
return nil, nil
62+
}
63+
64+
// DiagnoseFillableStructs computes diagnostics for fillable struct composite
65+
// literals overlapping with the provided start and end position.
66+
//
67+
// If either start or end is invalid, it is considered an unbounded condition.
68+
func DiagnoseFillableStructs(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic {
69+
var diags []analysis.Diagnostic
5370
nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)}
5471
inspect.Preorder(nodeFilter, func(n ast.Node) {
5572
expr := n.(*ast.CompositeLit)
5673

57-
// Find enclosing file.
58-
// TODO(adonovan): use inspect.WithStack?
59-
var file *ast.File
60-
for _, f := range pass.Files {
61-
if f.Pos() <= expr.Pos() && expr.Pos() <= f.End() {
62-
file = f
63-
break
64-
}
65-
}
66-
if file == nil {
67-
return
74+
if (start.IsValid() && expr.End() < start) || (end.IsValid() && expr.Pos() > end) {
75+
return // non-overlapping
6876
}
6977

70-
typ := pass.TypesInfo.TypeOf(expr)
78+
typ := info.TypeOf(expr)
7179
if typ == nil {
7280
return
7381
}
@@ -92,7 +100,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
92100
for i := 0; i < fieldCount; i++ {
93101
field := tStruct.Field(i)
94102
// Ignore fields that are not accessible in the current package.
95-
if field.Pkg() != nil && field.Pkg() != pass.Pkg && !field.Exported() {
103+
if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() {
96104
continue
97105
}
98106
fillableFields = append(fillableFields, fmt.Sprintf("%s: %s", field.Name(), field.Type().String()))
@@ -105,7 +113,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
105113
var name string
106114
if typ != tStruct {
107115
// named struct type (e.g. pkg.S[T])
108-
name = types.TypeString(typ, types.RelativeTo(pass.Pkg))
116+
name = types.TypeString(typ, types.RelativeTo(pkg))
109117
} else {
110118
// anonymous struct type
111119
totalFields := len(fillableFields)
@@ -124,13 +132,14 @@ func run(pass *analysis.Pass) (interface{}, error) {
124132
}
125133
name = fmt.Sprintf("anonymous struct { %s }", strings.Join(fillableFields, ", "))
126134
}
127-
pass.Report(analysis.Diagnostic{
135+
diags = append(diags, analysis.Diagnostic{
128136
Message: fmt.Sprintf("Fill %s", name),
129137
Pos: expr.Pos(),
130138
End: expr.End(),
131139
})
132140
})
133-
return nil, nil
141+
142+
return diags
134143
}
135144

136145
// SuggestedFix computes the suggested fix for the kinds of

gopls/internal/lsp/analysis/infertypeargs/infertypeargs.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
package infertypeargs
88

99
import (
10+
"go/token"
11+
1012
"golang.org/x/tools/go/analysis"
1113
"golang.org/x/tools/go/analysis/passes/inspect"
14+
"golang.org/x/tools/go/ast/inspector"
1215
)
1316

1417
const Doc = `check for unnecessary type arguments in call expressions
@@ -29,3 +32,16 @@ var Analyzer = &analysis.Analyzer{
2932
Requires: []*analysis.Analyzer{inspect.Analyzer},
3033
Run: run,
3134
}
35+
36+
// TODO(rfindley): remove this thin wrapper around the infertypeargs refactoring,
37+
// and eliminate the infertypeargs analyzer.
38+
//
39+
// Previous iterations used the analysis framework for computing refactorings,
40+
// which proved inefficient.
41+
func run(pass *analysis.Pass) (interface{}, error) {
42+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
43+
for _, diag := range DiagnoseInferableTypeArgs(pass.Fset, inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) {
44+
pass.Report(diag)
45+
}
46+
return nil, nil
47+
}

gopls/internal/lsp/analysis/infertypeargs/run_go117.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@
77

88
package infertypeargs
99

10-
import "golang.org/x/tools/go/analysis"
10+
import (
11+
"go/token"
12+
"go/types"
1113

12-
// This analyzer only relates to go1.18+, and uses the types.CheckExpr API that
13-
// was added in Go 1.13.
14-
func run(pass *analysis.Pass) (interface{}, error) {
15-
return nil, nil
14+
"golang.org/x/tools/go/analysis"
15+
"golang.org/x/tools/go/ast/inspector"
16+
)
17+
18+
// DiagnoseInferableTypeArgs returns an empty slice, as generics are not supported at
19+
// this go version.
20+
func DiagnoseInferableTypeArgs(fset *token.FileSet, inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic {
21+
return nil
1622
}

gopls/internal/lsp/analysis/infertypeargs/run_go118.go

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@ import (
1313
"go/types"
1414

1515
"golang.org/x/tools/go/analysis"
16-
"golang.org/x/tools/go/analysis/passes/inspect"
1716
"golang.org/x/tools/go/ast/inspector"
1817
"golang.org/x/tools/internal/typeparams"
1918
)
2019

21-
func run(pass *analysis.Pass) (interface{}, error) {
22-
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
23-
24-
nodeFilter := []ast.Node{
25-
(*ast.CallExpr)(nil),
26-
}
20+
// DiagnoseInferableTypeArgs reports diagnostics describing simplifications to type
21+
// arguments overlapping with the provided start and end position.
22+
//
23+
// If start or end is token.NoPos, the corresponding bound is not checked
24+
// (i.e. if both start and end are NoPos, all call expressions are considered).
25+
func DiagnoseInferableTypeArgs(fset *token.FileSet, inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic {
26+
var diags []analysis.Diagnostic
2727

28+
nodeFilter := []ast.Node{(*ast.CallExpr)(nil)}
2829
inspect.Preorder(nodeFilter, func(node ast.Node) {
2930
call := node.(*ast.CallExpr)
3031
x, lbrack, indices, rbrack := typeparams.UnpackIndexExpr(call.Fun)
@@ -33,8 +34,12 @@ func run(pass *analysis.Pass) (interface{}, error) {
3334
return // no explicit args, nothing to do
3435
}
3536

37+
if (start.IsValid() && call.End() < start) || (end.IsValid() && call.Pos() > end) {
38+
return // non-overlapping
39+
}
40+
3641
// Confirm that instantiation actually occurred at this ident.
37-
idata, ok := typeparams.GetInstances(pass.TypesInfo)[ident]
42+
idata, ok := typeparams.GetInstances(info)[ident]
3843
if !ok {
3944
return // something went wrong, but fail open
4045
}
@@ -60,7 +65,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
6065
}
6166
info := new(types.Info)
6267
typeparams.InitInstanceInfo(info)
63-
if err := types.CheckExpr(pass.Fset, pass.Pkg, call.Pos(), newCall, info); err != nil {
68+
if err := types.CheckExpr(fset, pkg, call.Pos(), newCall, info); err != nil {
6469
// Most likely inference failed.
6570
break
6671
}
@@ -74,20 +79,24 @@ func run(pass *analysis.Pass) (interface{}, error) {
7479
required = i
7580
}
7681
if required < len(indices) {
77-
var start, end token.Pos
82+
var s, e token.Pos
7883
var edit analysis.TextEdit
7984
if required == 0 {
80-
start, end = lbrack, rbrack+1 // erase the entire index
81-
edit = analysis.TextEdit{Pos: start, End: end}
85+
s, e = lbrack, rbrack+1 // erase the entire index
86+
edit = analysis.TextEdit{Pos: s, End: e}
8287
} else {
83-
start = indices[required].Pos()
84-
end = rbrack
88+
s = indices[required].Pos()
89+
e = rbrack
8590
// erase from end of last arg to include last comma & white-spaces
86-
edit = analysis.TextEdit{Pos: indices[required-1].End(), End: end}
91+
edit = analysis.TextEdit{Pos: indices[required-1].End(), End: e}
92+
}
93+
// Recheck that our (narrower) fixes overlap with the requested range.
94+
if (start.IsValid() && e < start) || (end.IsValid() && s > end) {
95+
return // non-overlapping
8796
}
88-
pass.Report(analysis.Diagnostic{
89-
Pos: start,
90-
End: end,
97+
diags = append(diags, analysis.Diagnostic{
98+
Pos: s,
99+
End: e,
91100
Message: "unnecessary type arguments",
92101
SuggestedFixes: []analysis.SuggestedFix{{
93102
Message: "simplify type arguments",
@@ -97,7 +106,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
97106
}
98107
})
99108

100-
return nil, nil
109+
return diags
101110
}
102111

103112
func calledIdent(x ast.Expr) *ast.Ident {

gopls/internal/lsp/analysis/stubmethods/stubmethods.go

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"strings"
1616

1717
"golang.org/x/tools/go/analysis"
18-
"golang.org/x/tools/go/analysis/passes/inspect"
1918
"golang.org/x/tools/go/ast/astutil"
2019
"golang.org/x/tools/internal/analysisinternal"
2120
"golang.org/x/tools/internal/typesinternal"
@@ -29,51 +28,72 @@ in order to implement a target interface`
2928
var Analyzer = &analysis.Analyzer{
3029
Name: "stubmethods",
3130
Doc: Doc,
32-
Requires: []*analysis.Analyzer{inspect.Analyzer},
3331
Run: run,
3432
RunDespiteErrors: true,
3533
}
3634

35+
// TODO(rfindley): remove this thin wrapper around the stubmethods refactoring,
36+
// and eliminate the stubmethods analyzer.
37+
//
38+
// Previous iterations used the analysis framework for computing refactorings,
39+
// which proved inefficient.
3740
func run(pass *analysis.Pass) (interface{}, error) {
3841
for _, err := range pass.TypeErrors {
39-
ifaceErr := strings.Contains(err.Msg, "missing method") || strings.HasPrefix(err.Msg, "cannot convert")
40-
if !ifaceErr {
41-
continue
42-
}
4342
var file *ast.File
4443
for _, f := range pass.Files {
4544
if f.Pos() <= err.Pos && err.Pos < f.End() {
4645
file = f
4746
break
4847
}
4948
}
50-
if file == nil {
51-
continue
52-
}
5349
// Get the end position of the error.
54-
_, _, endPos, ok := typesinternal.ReadGo116ErrorData(err)
50+
_, _, end, ok := typesinternal.ReadGo116ErrorData(err)
5551
if !ok {
5652
var buf bytes.Buffer
5753
if err := format.Node(&buf, pass.Fset, file); err != nil {
5854
continue
5955
}
60-
endPos = analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos)
56+
end = analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos)
6157
}
62-
path, _ := astutil.PathEnclosingInterval(file, err.Pos, endPos)
63-
si := GetStubInfo(pass.Fset, pass.TypesInfo, path, err.Pos)
64-
if si == nil {
65-
continue
58+
if diag, ok := DiagnosticForError(pass.Fset, file, err.Pos, end, err.Msg, pass.TypesInfo); ok {
59+
pass.Report(diag)
6660
}
67-
qf := RelativeToFiles(si.Concrete.Obj().Pkg(), file, nil, nil)
68-
pass.Report(analysis.Diagnostic{
69-
Pos: err.Pos,
70-
End: endPos,
71-
Message: fmt.Sprintf("Implement %s", types.TypeString(si.Interface.Type(), qf)),
72-
})
7361
}
62+
7463
return nil, nil
7564
}
7665

66+
// MatchesMessage reports whether msg matches the error message sought after by
67+
// the stubmethods fix.
68+
func MatchesMessage(msg string) bool {
69+
return strings.Contains(msg, "missing method") || strings.HasPrefix(msg, "cannot convert")
70+
}
71+
72+
// DiagnosticForError computes a diagnostic suggesting to implement an
73+
// interface to fix the type checking error defined by (start, end, msg).
74+
//
75+
// If no such fix is possible, the second result is false.
76+
//
77+
// TODO(rfindley): simplify this signature once the stubmethods refactoring is
78+
// no longer wedged into the analysis framework.
79+
func DiagnosticForError(fset *token.FileSet, file *ast.File, start, end token.Pos, msg string, info *types.Info) (analysis.Diagnostic, bool) {
80+
if !MatchesMessage(msg) {
81+
return analysis.Diagnostic{}, false
82+
}
83+
84+
path, _ := astutil.PathEnclosingInterval(file, start, end)
85+
si := GetStubInfo(fset, info, path, start)
86+
if si == nil {
87+
return analysis.Diagnostic{}, false
88+
}
89+
qf := RelativeToFiles(si.Concrete.Obj().Pkg(), file, nil, nil)
90+
return analysis.Diagnostic{
91+
Pos: start,
92+
End: end,
93+
Message: fmt.Sprintf("Implement %s", types.TypeString(si.Interface.Type(), qf)),
94+
}, true
95+
}
96+
7797
// StubInfo represents a concrete type
7898
// that wants to stub out an interface type
7999
type StubInfo struct {

gopls/internal/lsp/cache/pkg.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,12 @@ func (p *Package) DependencyTypes(path source.PackagePath) *types.Package {
153153
return p.pkg.importMap[path]
154154
}
155155

156-
func (p *Package) HasParseErrors() bool {
157-
return len(p.pkg.parseErrors) != 0
156+
func (p *Package) GetParseErrors() []scanner.ErrorList {
157+
return p.pkg.parseErrors
158158
}
159159

160-
func (p *Package) HasTypeErrors() bool {
161-
return len(p.pkg.typeErrors) != 0
160+
func (p *Package) GetTypeErrors() []types.Error {
161+
return p.pkg.typeErrors
162162
}
163163

164164
func (p *Package) DiagnosticsForFile(ctx context.Context, s source.Snapshot, uri span.URI) ([]*source.Diagnostic, error) {

0 commit comments

Comments
 (0)