Skip to content

Commit daa4aa5

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/lsp/source: stubmethods: fix out-of-bounds index
The attatchedd test case spuriously triggered stubmethods (the "cannot convert" error is about something else) in a function whose return an functype arities were mismatched. The new behavior is "nothing happens", no code actions are offered, nothing is logged, and no suggested fix is applied, or fails to apply. (Along the way I implemented suggestedfixerr before I realized it would not be useful; but it may be useful later.) Fixes golang/go#64087 Change-Id: I27e825248576f9bda2229c652487fdbec9a25bc2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/543018 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent a586d0d commit daa4aa5

File tree

3 files changed

+93
-9
lines changed

3 files changed

+93
-9
lines changed

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,25 +203,30 @@ func fromCallExpr(fset *token.FileSet, ti *types.Info, pos token.Pos, ce *ast.Ca
203203
//
204204
// For example, func() io.Writer { return myType{} }
205205
// would return StubInfo with the interface being io.Writer and the concrete type being myType{}.
206-
func fromReturnStmt(fset *token.FileSet, ti *types.Info, pos token.Pos, path []ast.Node, rs *ast.ReturnStmt) (*StubInfo, error) {
206+
func fromReturnStmt(fset *token.FileSet, ti *types.Info, pos token.Pos, path []ast.Node, ret *ast.ReturnStmt) (*StubInfo, error) {
207207
returnIdx := -1
208-
for i, r := range rs.Results {
208+
for i, r := range ret.Results {
209209
if pos >= r.Pos() && pos <= r.End() {
210210
returnIdx = i
211211
}
212212
}
213213
if returnIdx == -1 {
214-
return nil, fmt.Errorf("pos %d not within return statement bounds: [%d-%d]", pos, rs.Pos(), rs.End())
214+
return nil, fmt.Errorf("pos %d not within return statement bounds: [%d-%d]", pos, ret.Pos(), ret.End())
215215
}
216-
concObj, pointer := concreteType(rs.Results[returnIdx], ti)
216+
concObj, pointer := concreteType(ret.Results[returnIdx], ti)
217217
if concObj == nil || concObj.Obj().Pkg() == nil {
218218
return nil, nil
219219
}
220-
ef := enclosingFunction(path, ti)
221-
if ef == nil {
220+
funcType := enclosingFunction(path, ti)
221+
if funcType == nil {
222222
return nil, fmt.Errorf("could not find the enclosing function of the return statement")
223223
}
224-
iface := ifaceType(ef.Results.List[returnIdx].Type, ti)
224+
if len(funcType.Results.List) != len(ret.Results) {
225+
return nil, fmt.Errorf("%d-operand return statement in %d-result function",
226+
len(ret.Results),
227+
len(funcType.Results.List))
228+
}
229+
iface := ifaceType(funcType.Results.List[returnIdx].Type, ti)
225230
if iface == nil {
226231
return nil, nil
227232
}

gopls/internal/lsp/regtest/marker.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ var update = flag.Bool("update", false, "if set, update test data during marker
263263
// This action is executed for its editing effects on the source files.
264264
// Like rename, the golden directory contains the expected transformed files.
265265
//
266+
// - suggestedfixerr(location, regexp, kind, wantError): specifies that the
267+
// suggestedfix operation should fail with an error that matches the expectation.
268+
// (Failures in the computation to offer a fix do not generally result
269+
// in LSP errors, so this marker is not appropriate for testing them.)
270+
//
266271
// - rank(location, ...completionItem): executes a textDocument/completion
267272
// request at the given location, and verifies that each expected
268273
// completion item occurs in the results, in the expected order. Other
@@ -770,6 +775,7 @@ var actionMarkerFuncs = map[string]func(marker){
770775
"signature": actionMarkerFunc(signatureMarker),
771776
"snippet": actionMarkerFunc(snippetMarker),
772777
"suggestedfix": actionMarkerFunc(suggestedfixMarker),
778+
"suggestedfixerr": actionMarkerFunc(suggestedfixErrMarker),
773779
"symbol": actionMarkerFunc(symbolMarker),
774780
"token": actionMarkerFunc(tokenMarker),
775781
"typedef": actionMarkerFunc(typedefMarker),
@@ -2139,6 +2145,20 @@ func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, g
21392145
checkDiffs(mark, changed, golden)
21402146
}
21412147

2148+
func suggestedfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp, wantErr wantError) {
2149+
loc.Range.End = loc.Range.Start // diagnostics ignore end position.
2150+
// Find and remove the matching diagnostic.
2151+
diag, ok := removeDiagnostic(mark, loc, re)
2152+
if !ok {
2153+
mark.errorf("no diagnostic at %v matches %q", loc, re)
2154+
return
2155+
}
2156+
2157+
// Apply the fix it suggests.
2158+
_, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil)
2159+
wantErr.check(mark, err)
2160+
}
2161+
21422162
// codeAction executes a textDocument/codeAction request for the specified
21432163
// location and kind. If diag is non-nil, it is used as the code action
21442164
// context.
@@ -2254,8 +2274,6 @@ func codeActionChanges(env *Env, uri protocol.DocumentURI, rng protocol.Range, a
22542274
return nil, nil
22552275
}
22562276

2257-
// TODO(adonovan): suggestedfixerr
2258-
22592277
// refsMarker implements the @refs marker.
22602278
func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) {
22612279
refs := func(includeDeclaration bool, want []protocol.Location) error {

gopls/internal/regtest/workspace/quickfix_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,64 @@ func main() {}
331331
})
332332
}
333333
}
334+
335+
func TestStubMethods64087(t *testing.T) {
336+
// We can't use the @fix or @suggestedfixerr or @codeactionerr
337+
// because the error now reported by the corrected logic
338+
// is internal and silently causes no fix to be offered.
339+
340+
const files = `
341+
This is a regression test for a panic (issue #64087) in stub methods.
342+
343+
The illegal expression int("") caused a "cannot convert" error that
344+
spuriously triggered the "stub methods" in a function whose return
345+
statement had too many operands, leading to an out-of-bounds index.
346+
347+
-- go.mod --
348+
module mod.com
349+
go 1.18
350+
351+
-- a.go --
352+
package a
353+
354+
func f() error {
355+
return nil, myerror{int("")}
356+
}
357+
358+
type myerror struct{any}
359+
`
360+
Run(t, files, func(t *testing.T, env *Env) {
361+
env.OpenFile("a.go")
362+
363+
// Expect a "wrong result count" diagnostic.
364+
var d protocol.PublishDiagnosticsParams
365+
env.AfterChange(ReadDiagnostics("a.go", &d))
366+
367+
// In no particular order, we expect:
368+
// "...too many return values..." (compiler)
369+
// "...cannot convert..." (compiler)
370+
// and possibly:
371+
// "...too many return values..." (fillreturns)
372+
// We check only for the first of these.
373+
found := false
374+
for i, diag := range d.Diagnostics {
375+
t.Logf("Diagnostics[%d] = %q (%s)", i, diag.Message, diag.Source)
376+
if strings.Contains(diag.Message, "too many return") {
377+
found = true
378+
}
379+
}
380+
if !found {
381+
t.Fatalf("Expected WrongResultCount diagnostic not found.")
382+
}
383+
384+
// GetQuickFixes should not panic (the original bug).
385+
fixes := env.GetQuickFixes("a.go", d.Diagnostics)
386+
387+
// We should not be offered a "stub methods" fix.
388+
for _, fix := range fixes {
389+
if strings.Contains(fix.Title, "Implement error") {
390+
t.Errorf("unexpected 'stub methods' fix: %#v", fix)
391+
}
392+
}
393+
})
394+
}

0 commit comments

Comments
 (0)