Skip to content

Commit f40889d

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/analysis/stubmethods: fix OOB panic in fromValueSpec
The loop over ValueSpec.Rhs assumed it was nonempty, but it's possible to spuriously trigger a "cannot convert" error when the problem is on the LHS. (I don't know if the attached test case is what caused the panic in the field, but it seems plausible.) Added a test, similar to the one to fix golang/go#64087. Also, audit for similar mistakes, and tidy the code up, to use conventional variable names and simpler logic. Fixes golang/go#64545 Change-Id: I49851435e7a363641a2844620633099b11f1e414 Reviewed-on: https://go-review.googlesource.com/c/tools/+/548737 Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 113a081 commit f40889d

File tree

2 files changed

+138
-74
lines changed

2 files changed

+138
-74
lines changed

gopls/internal/analysis/stubmethods/stubmethods.go

+82-74
Original file line numberDiff line numberDiff line change
@@ -119,26 +119,26 @@ type StubInfo struct {
119119
// function call. This is essentially what the refactor/satisfy does,
120120
// more generally. Refactor to share logic, after auditing 'satisfy'
121121
// for safety on ill-typed code.
122-
func GetStubInfo(fset *token.FileSet, ti *types.Info, path []ast.Node, pos token.Pos) *StubInfo {
122+
func GetStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node, pos token.Pos) *StubInfo {
123123
for _, n := range path {
124124
switch n := n.(type) {
125125
case *ast.ValueSpec:
126-
return fromValueSpec(fset, ti, n, pos)
126+
return fromValueSpec(fset, info, n, pos)
127127
case *ast.ReturnStmt:
128128
// An error here may not indicate a real error the user should know about, but it may.
129129
// Therefore, it would be best to log it out for debugging/reporting purposes instead of ignoring
130130
// it. However, event.Log takes a context which is not passed via the analysis package.
131131
// TODO(marwan-at-work): properly log this error.
132-
si, _ := fromReturnStmt(fset, ti, pos, path, n)
132+
si, _ := fromReturnStmt(fset, info, pos, path, n)
133133
return si
134134
case *ast.AssignStmt:
135-
return fromAssignStmt(fset, ti, n, pos)
135+
return fromAssignStmt(fset, info, n, pos)
136136
case *ast.CallExpr:
137137
// Note that some call expressions don't carry the interface type
138138
// because they don't point to a function or method declaration elsewhere.
139139
// For eaxmple, "var Interface = (*Concrete)(nil)". In that case, continue
140140
// this loop to encounter other possibilities such as *ast.ValueSpec or others.
141-
si := fromCallExpr(fset, ti, pos, n)
141+
si := fromCallExpr(fset, info, pos, n)
142142
if si != nil {
143143
return si
144144
}
@@ -150,23 +150,26 @@ func GetStubInfo(fset *token.FileSet, ti *types.Info, path []ast.Node, pos token
150150
// fromCallExpr tries to find an *ast.CallExpr's function declaration and
151151
// analyzes a function call's signature against the passed in parameter to deduce
152152
// the concrete and interface types.
153-
func fromCallExpr(fset *token.FileSet, ti *types.Info, pos token.Pos, ce *ast.CallExpr) *StubInfo {
154-
paramIdx := -1
155-
for i, p := range ce.Args {
156-
if pos >= p.Pos() && pos <= p.End() {
157-
paramIdx = i
153+
func fromCallExpr(fset *token.FileSet, info *types.Info, pos token.Pos, call *ast.CallExpr) *StubInfo {
154+
// Find argument containing pos.
155+
argIdx := -1
156+
var arg ast.Expr
157+
for i, callArg := range call.Args {
158+
if callArg.Pos() <= pos && pos <= callArg.End() {
159+
argIdx = i
160+
arg = callArg
158161
break
159162
}
160163
}
161-
if paramIdx == -1 {
164+
if arg == nil {
162165
return nil
163166
}
164-
p := ce.Args[paramIdx]
165-
concObj, pointer := concreteType(p, ti)
166-
if concObj == nil || concObj.Obj().Pkg() == nil {
167+
168+
concType, pointer := concreteType(arg, info)
169+
if concType == nil || concType.Obj().Pkg() == nil {
167170
return nil
168171
}
169-
tv, ok := ti.Types[ce.Fun]
172+
tv, ok := info.Types[call.Fun]
170173
if !ok {
171174
return nil
172175
}
@@ -175,13 +178,13 @@ func fromCallExpr(fset *token.FileSet, ti *types.Info, pos token.Pos, ce *ast.Ca
175178
return nil
176179
}
177180
var paramType types.Type
178-
if sig.Variadic() && paramIdx >= sig.Params().Len()-1 {
181+
if sig.Variadic() && argIdx >= sig.Params().Len()-1 {
179182
v := sig.Params().At(sig.Params().Len() - 1)
180183
if s, _ := v.Type().(*types.Slice); s != nil {
181184
paramType = s.Elem()
182185
}
183-
} else if paramIdx < sig.Params().Len() {
184-
paramType = sig.Params().At(paramIdx).Type()
186+
} else if argIdx < sig.Params().Len() {
187+
paramType = sig.Params().At(argIdx).Type()
185188
}
186189
if paramType == nil {
187190
return nil // A type error prevents us from determining the param type.
@@ -192,7 +195,7 @@ func fromCallExpr(fset *token.FileSet, ti *types.Info, pos token.Pos, ce *ast.Ca
192195
}
193196
return &StubInfo{
194197
Fset: fset,
195-
Concrete: concObj,
198+
Concrete: concType,
196199
Pointer: pointer,
197200
Interface: iface,
198201
}
@@ -203,21 +206,24 @@ func fromCallExpr(fset *token.FileSet, ti *types.Info, pos token.Pos, ce *ast.Ca
203206
//
204207
// For example, func() io.Writer { return myType{} }
205208
// 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, ret *ast.ReturnStmt) (*StubInfo, error) {
209+
func fromReturnStmt(fset *token.FileSet, info *types.Info, pos token.Pos, path []ast.Node, ret *ast.ReturnStmt) (*StubInfo, error) {
210+
// Find return operand containing pos.
207211
returnIdx := -1
208212
for i, r := range ret.Results {
209-
if pos >= r.Pos() && pos <= r.End() {
213+
if r.Pos() <= pos && pos <= r.End() {
210214
returnIdx = i
215+
break
211216
}
212217
}
213218
if returnIdx == -1 {
214219
return nil, fmt.Errorf("pos %d not within return statement bounds: [%d-%d]", pos, ret.Pos(), ret.End())
215220
}
216-
concObj, pointer := concreteType(ret.Results[returnIdx], ti)
217-
if concObj == nil || concObj.Obj().Pkg() == nil {
221+
222+
concType, pointer := concreteType(ret.Results[returnIdx], info)
223+
if concType == nil || concType.Obj().Pkg() == nil {
218224
return nil, nil
219225
}
220-
funcType := enclosingFunction(path, ti)
226+
funcType := enclosingFunction(path, info)
221227
if funcType == nil {
222228
return nil, fmt.Errorf("could not find the enclosing function of the return statement")
223229
}
@@ -226,86 +232,91 @@ func fromReturnStmt(fset *token.FileSet, ti *types.Info, pos token.Pos, path []a
226232
len(ret.Results),
227233
len(funcType.Results.List))
228234
}
229-
iface := ifaceType(funcType.Results.List[returnIdx].Type, ti)
235+
iface := ifaceType(funcType.Results.List[returnIdx].Type, info)
230236
if iface == nil {
231237
return nil, nil
232238
}
233239
return &StubInfo{
234240
Fset: fset,
235-
Concrete: concObj,
241+
Concrete: concType,
236242
Pointer: pointer,
237243
Interface: iface,
238244
}, nil
239245
}
240246

241247
// fromValueSpec returns *StubInfo from a variable declaration such as
242248
// var x io.Writer = &T{}
243-
func fromValueSpec(fset *token.FileSet, ti *types.Info, vs *ast.ValueSpec, pos token.Pos) *StubInfo {
244-
var idx int
245-
for i, vs := range vs.Values {
246-
if pos >= vs.Pos() && pos <= vs.End() {
247-
idx = i
249+
func fromValueSpec(fset *token.FileSet, info *types.Info, spec *ast.ValueSpec, pos token.Pos) *StubInfo {
250+
// Find RHS element containing pos.
251+
var rhs ast.Expr
252+
for _, r := range spec.Values {
253+
if r.Pos() <= pos && pos <= r.End() {
254+
rhs = r
248255
break
249256
}
250257
}
258+
if rhs == nil {
259+
return nil // e.g. pos was on the LHS (#64545)
260+
}
251261

252-
valueNode := vs.Values[idx]
253-
ifaceNode := vs.Type
254-
callExp, ok := valueNode.(*ast.CallExpr)
255-
// if the ValueSpec is `var _ = myInterface(...)`
256-
// as opposed to `var _ myInterface = ...`
257-
if ifaceNode == nil && ok && len(callExp.Args) == 1 {
258-
ifaceNode = callExp.Fun
259-
valueNode = callExp.Args[0]
260-
}
261-
concObj, pointer := concreteType(valueNode, ti)
262-
if concObj == nil || concObj.Obj().Pkg() == nil {
262+
// Possible implicit/explicit conversion to interface type?
263+
ifaceNode := spec.Type // var _ myInterface = ...
264+
if call, ok := rhs.(*ast.CallExpr); ok && ifaceNode == nil && len(call.Args) == 1 {
265+
// var _ = myInterface(v)
266+
ifaceNode = call.Fun
267+
rhs = call.Args[0]
268+
}
269+
concType, pointer := concreteType(rhs, info)
270+
if concType == nil || concType.Obj().Pkg() == nil {
263271
return nil
264272
}
265-
ifaceObj := ifaceType(ifaceNode, ti)
273+
ifaceObj := ifaceType(ifaceNode, info)
266274
if ifaceObj == nil {
267275
return nil
268276
}
269277
return &StubInfo{
270278
Fset: fset,
271-
Concrete: concObj,
279+
Concrete: concType,
272280
Interface: ifaceObj,
273281
Pointer: pointer,
274282
}
275283
}
276284

277-
// fromAssignStmt returns *StubInfo from a variable re-assignment such as
285+
// fromAssignStmt returns *StubInfo from a variable assignment such as
278286
// var x io.Writer
279287
// x = &T{}
280-
func fromAssignStmt(fset *token.FileSet, ti *types.Info, as *ast.AssignStmt, pos token.Pos) *StubInfo {
281-
idx := -1
288+
func fromAssignStmt(fset *token.FileSet, info *types.Info, assign *ast.AssignStmt, pos token.Pos) *StubInfo {
289+
// The interface conversion error in an assignment is against the RHS:
290+
//
291+
// var x io.Writer
292+
// x = &T{} // error: missing method
293+
// ^^^^
294+
//
295+
// Find RHS element containing pos.
282296
var lhs, rhs ast.Expr
283-
// Given a re-assignment interface conversion error,
284-
// the compiler error shows up on the right hand side of the expression.
285-
// For example, x = &T{} where x is io.Writer highlights the error
286-
// under "&T{}" and not "x".
287-
for i, hs := range as.Rhs {
288-
if pos >= hs.Pos() && pos <= hs.End() {
289-
idx = i
297+
for i, r := range assign.Rhs {
298+
if r.Pos() <= pos && pos <= r.End() {
299+
if i >= len(assign.Lhs) {
300+
// This should never happen as we would get a
301+
// "cannot assign N values to M variables"
302+
// before we get an interface conversion error.
303+
// But be defensive.
304+
return nil
305+
}
306+
lhs = assign.Lhs[i]
307+
rhs = r
290308
break
291309
}
292310
}
293-
if idx == -1 {
311+
if lhs == nil || rhs == nil {
294312
return nil
295313
}
296-
// Technically, this should never happen as
297-
// we would get a "cannot assign N values to M variables"
298-
// before we get an interface conversion error. Nonetheless,
299-
// guard against out of range index errors.
300-
if idx >= len(as.Lhs) {
301-
return nil
302-
}
303-
lhs, rhs = as.Lhs[idx], as.Rhs[idx]
304-
ifaceObj := ifaceType(lhs, ti)
314+
315+
ifaceObj := ifaceType(lhs, info)
305316
if ifaceObj == nil {
306317
return nil
307318
}
308-
concType, pointer := concreteType(rhs, ti)
319+
concType, pointer := concreteType(rhs, info)
309320
if concType == nil || concType.Obj().Pkg() == nil {
310321
return nil
311322
}
@@ -382,11 +393,9 @@ func RelativeToFiles(concPkg *types.Package, concFile *ast.File, ifaceImports []
382393
}
383394
}
384395

385-
// ifaceType will try to extract the types.Object that defines
386-
// the interface given the ast.Expr where the "missing method"
387-
// or "conversion" errors happen.
388-
func ifaceType(n ast.Expr, ti *types.Info) *types.TypeName {
389-
tv, ok := ti.Types[n]
396+
// ifaceType returns the named interface type to which e refers, if any.
397+
func ifaceType(e ast.Expr, info *types.Info) *types.TypeName {
398+
tv, ok := info.Types[e]
390399
if !ok {
391400
return nil
392401
}
@@ -398,8 +407,7 @@ func ifaceObjFromType(t types.Type) *types.TypeName {
398407
if !ok {
399408
return nil
400409
}
401-
_, ok = named.Underlying().(*types.Interface)
402-
if !ok {
410+
if !types.IsInterface(named) {
403411
return nil
404412
}
405413
// Interfaces defined in the "builtin" package return nil a Pkg().
@@ -418,8 +426,8 @@ func ifaceObjFromType(t types.Type) *types.TypeName {
418426
// method will return a nil *types.Named. The second return parameter
419427
// is a boolean that indicates whether the concreteType was defined as a
420428
// pointer or value.
421-
func concreteType(n ast.Expr, ti *types.Info) (*types.Named, bool) {
422-
tv, ok := ti.Types[n]
429+
func concreteType(e ast.Expr, info *types.Info) (*types.Named, bool) {
430+
tv, ok := info.Types[e]
423431
if !ok {
424432
return nil, false
425433
}

gopls/internal/test/integration/workspace/quickfix_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ func TestStubMethods64087(t *testing.T) {
336336
// We can't use the @fix or @suggestedfixerr or @codeactionerr
337337
// because the error now reported by the corrected logic
338338
// is internal and silently causes no fix to be offered.
339+
//
340+
// See also the similar TestStubMethods64545 below.
339341

340342
const files = `
341343
This is a regression test for a panic (issue #64087) in stub methods.
@@ -392,3 +394,57 @@ type myerror struct{any}
392394
}
393395
})
394396
}
397+
398+
func TestStubMethods64545(t *testing.T) {
399+
// We can't use the @fix or @suggestedfixerr or @codeactionerr
400+
// because the error now reported by the corrected logic
401+
// is internal and silently causes no fix to be offered.
402+
//
403+
// TODO(adonovan): we may need to generalize this test and
404+
// TestStubMethods64087 if this happens a lot.
405+
406+
const files = `
407+
This is a regression test for a panic (issue #64545) in stub methods.
408+
409+
The illegal expression int("") caused a "cannot convert" error that
410+
spuriously triggered the "stub methods" in a function whose var
411+
spec had no RHS values, leading to an out-of-bounds index.
412+
413+
-- go.mod --
414+
module mod.com
415+
go 1.18
416+
417+
-- a.go --
418+
package a
419+
420+
var _ [int("")]byte
421+
`
422+
Run(t, files, func(t *testing.T, env *Env) {
423+
env.OpenFile("a.go")
424+
425+
// Expect a "cannot convert" diagnostic, and perhaps others.
426+
var d protocol.PublishDiagnosticsParams
427+
env.AfterChange(ReadDiagnostics("a.go", &d))
428+
429+
found := false
430+
for i, diag := range d.Diagnostics {
431+
t.Logf("Diagnostics[%d] = %q (%s)", i, diag.Message, diag.Source)
432+
if strings.Contains(diag.Message, "cannot convert") {
433+
found = true
434+
}
435+
}
436+
if !found {
437+
t.Fatalf("Expected 'cannot convert' diagnostic not found.")
438+
}
439+
440+
// GetQuickFixes should not panic (the original bug).
441+
fixes := env.GetQuickFixes("a.go", d.Diagnostics)
442+
443+
// We should not be offered a "stub methods" fix.
444+
for _, fix := range fixes {
445+
if strings.Contains(fix.Title, "Implement error") {
446+
t.Errorf("unexpected 'stub methods' fix: %#v", fix)
447+
}
448+
}
449+
})
450+
}

0 commit comments

Comments
 (0)