Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x/tools/gopls: implement struct field generation quickfix #544

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
30 changes: 29 additions & 1 deletion gopls/doc/features/diagnostics.md
Original file line number Diff line number Diff line change
@@ -72,7 +72,6 @@ There is an optional third source of diagnostics:
are transitively free from errors, so optimization diagnostics
will not be shown on packages that do not build.


## Recomputation of diagnostics

By default, diagnostics are automatically recomputed each time the source files
@@ -272,6 +271,35 @@ func doSomething(i int) string {
panic("unimplemented")
}
```

### `StubMissingStructField`: Declare missing field T.f

When you attempt to access a field on a type that does not have the field,
the compiler will report an error such as "type X has no field or method Y".
In this scenario, gopls now offers a quick fix to generate a stub declaration of
the missing field, inferring its type from the context in which it is used.

Consider the following code where `Foo` does not have a field `bar`:

```go
type Foo struct{}

func main() {
var s string
f := Foo{}
s = f.bar // error: f.bar undefined (type Foo has no field or method bar)
}
```

Gopls will offer a quick fix, "Declare missing field Foo.bar".
When invoked, it creates the following declaration:

```go
type Foo struct{
bar string
}
```

<!--

dorky details and deletia:
21 changes: 21 additions & 0 deletions gopls/doc/release/v0.17.0.md
Original file line number Diff line number Diff line change
@@ -196,3 +196,24 @@ causing `Add` to race with `Wait`.
(This check is equivalent to
[staticcheck's SA2000](https://staticcheck.dev/docs/checks#SA2000),
but is enabled by default.)

## Add test for function or method

If the selected chunk of code is part of a function or method declaration F,
gopls will offer the "Add test for F" code action, which adds a new test for the
selected function in the corresponding `_test.go` file. The generated test takes
into account its signature, including input parameters and results.

Since this feature is implemented by the server (gopls), it is compatible with
all LSP-compliant editors. VS Code users may continue to use the client-side
`Go: Generate Unit Tests For file/function/package` command which uses the
[gotests](https://github.com/cweill/gotests) tool.

## Generate missing struct field from access

When you attempt to access a field on a type that does not have the field,
the compiler will report an error like “type X has no field or method Y”.
Gopls now offers a new code action, “Declare missing field of T.f”,
where T is the concrete type and f is the undefined field.
The stub field's signature is inferred
from the context of the access.
15 changes: 12 additions & 3 deletions gopls/internal/golang/codeaction.go
Original file line number Diff line number Diff line change
@@ -41,7 +41,6 @@ import (
//
// See ../protocol/codeactionkind.go for some code action theory.
func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool, trigger protocol.CodeActionTriggerKind) (actions []protocol.CodeAction, _ error) {

loc := protocol.Location{URI: fh.URI(), Range: rng}

pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full)
@@ -335,13 +334,23 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
}

// "type X has no field or method Y" compiler error.
// Offer a "Declare missing method T.f" code action.
// See [stubMissingCalledFunctionFixer] for command implementation.
case strings.Contains(msg, "has no field or method"):
// Offer a "Declare missing method T.f" code action.
// See [stubMissingCalledFunctionFixer] for command implementation.
si := stubmethods.GetCallStubInfo(req.pkg.FileSet(), info, req.pgf, start, end)
if si != nil {
msg := fmt.Sprintf("Declare missing method %s.%s", si.Receiver.Obj().Name(), si.MethodName)
req.addApplyFixAction(msg, fixMissingCalledFunction, req.loc)
} else {
fi := stubmethods.GetFieldStubInfo(req.pkg.FileSet(), info, req.pgf, start, end)
if fi != nil {
msg := fmt.Sprintf("Declare missing struct field %s.%s", fi.Named.Obj().Name(), fi.Expr.Sel.Name)
req.addApplyFixAction(msg, fixMissingStructField, req.loc)

// undeclared field might be a method
// msg = fmt.Sprintf("Declare missing method %s.%s", fi.Named.Obj().Name(), fi.Expr.Sel.Name)
// req.addApplyFixAction(msg, fixMissingCalledFunction, req.loc)
}
}

// "undeclared name: X" or "undefined: X" compiler error.
2 changes: 2 additions & 0 deletions gopls/internal/golang/fix.go
Original file line number Diff line number Diff line change
@@ -63,6 +63,7 @@ const (
fixCreateUndeclared = "create_undeclared"
fixMissingInterfaceMethods = "stub_missing_interface_method"
fixMissingCalledFunction = "stub_missing_called_function"
fixMissingStructField = "stub_missing_struct_field"
)

// ApplyFix applies the specified kind of suggested fix to the given
@@ -109,6 +110,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file
fixCreateUndeclared: singleFile(createUndeclared),
fixMissingInterfaceMethods: stubMissingInterfaceMethodsFixer,
fixMissingCalledFunction: stubMissingCalledFunctionFixer,
fixMissingStructField: stubMissingStructFieldFixer,
}
fixer, ok := fixers[fix]
if !ok {
228 changes: 158 additions & 70 deletions gopls/internal/golang/stub.go
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import (
"bytes"
"context"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
@@ -49,6 +50,17 @@ func stubMissingCalledFunctionFixer(ctx context.Context, snapshot *cache.Snapsho
return insertDeclsAfter(ctx, snapshot, pkg.Metadata(), si.Fset, si.After, si.Emit)
}

// stubMissingStructFieldFixer returns a suggested fix to declare the missing
// field that the user may want to generate based on SelectorExpr
// at the cursor position.
func stubMissingStructFieldFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
fi := stubmethods.GetFieldStubInfo(pkg.FileSet(), pkg.TypesInfo(), pgf, start, end)
if fi == nil {
return nil, nil, fmt.Errorf("invalid type request")
}
return insertStructField(ctx, snapshot, pkg.Metadata(), fi)
}

// An emitter writes new top-level declarations into an existing
// file. References to symbols should be qualified using qual, which
// respects the local import environment.
@@ -81,76 +93,10 @@ func insertDeclsAfter(ctx context.Context, snapshot *cache.Snapshot, mp *metadat
return nil, nil, bug.Errorf("can't find metadata for file %s among dependencies of %s", declPGF.URI, mp)
}

// Build import environment for the declaring file.
// (typesinternal.FileQualifier works only for complete
// import mappings, and requires types.)
importEnv := make(map[ImportPath]string) // value is local name
for _, imp := range declPGF.File.Imports {
importPath := metadata.UnquoteImportPath(imp)
var name string
if imp.Name != nil {
name = imp.Name.Name
if name == "_" {
continue
} else if name == "." {
name = "" // see types.Qualifier
}
} else {
// Use the correct name from the metadata of the imported
// package---not a guess based on the import path.
mp := snapshot.Metadata(declMeta.DepsByImpPath[importPath])
if mp == nil {
continue // can't happen?
}
name = string(mp.Name)
}
importEnv[importPath] = name // latest alias wins
}

// Create a package name qualifier that uses the
// locally appropriate imported package name.
// It records any needed new imports.
// TODO(adonovan): factor with golang.FormatVarType?
//
// Prior to CL 469155 this logic preserved any renaming
// imports from the file that declares the interface
// method--ostensibly the preferred name for imports of
// frequently renamed packages such as protobufs.
// Now we use the package's declared name. If this turns out
// to be a mistake, then use parseHeader(si.iface.Pos()).
//
type newImport struct{ name, importPath string }
var newImports []newImport // for AddNamedImport
qual := func(pkg *types.Package) string {
// TODO(adonovan): don't ignore vendor prefix.
//
// Ignore the current package import.
if pkg.Path() == sym.Pkg().Path() {
return ""
}

importPath := ImportPath(pkg.Path())
name, ok := importEnv[importPath]
if !ok {
// Insert new import using package's declared name.
//
// TODO(adonovan): resolve conflict between declared
// name and existing file-level (declPGF.File.Imports)
// or package-level (sym.Pkg.Scope) decls by
// generating a fresh name.
name = pkg.Name()
importEnv[importPath] = name
new := newImport{importPath: string(importPath)}
// For clarity, use a renaming import whenever the
// local name does not match the path's last segment.
if name != pathpkg.Base(trimVersionSuffix(new.importPath)) {
new.name = name
}
newImports = append(newImports, new)
}
return name
}

newImports := make([]newImport, 0, len(declPGF.File.Imports))
qual := newNamedImportQual(declPGF, snapshot, declMeta, sym, func(imp newImport) {
newImports = append(newImports, imp)
})
// Compute insertion point for new declarations:
// after the top-level declaration enclosing the (package-level) type.
insertOffset, err := safetoken.Offset(declPGF.Tok, declPGF.File.End())
@@ -236,3 +182,145 @@ func trimVersionSuffix(path string) string {
}
return path
}

func insertStructField(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.Package, fieldInfo *stubmethods.StructFieldInfo) (*token.FileSet, *analysis.SuggestedFix, error) {
if fieldInfo == nil {
return nil, nil, fmt.Errorf("no field info provided")
}

// get the file containing the struct definition using the position
declPGF, _, err := parseFull(ctx, snapshot, fieldInfo.Fset, fieldInfo.Named.Obj().Pos())
if err != nil {
return nil, nil, fmt.Errorf("failed to parse file declaring struct: %w", err)
}
if declPGF.Fixed() {
return nil, nil, fmt.Errorf("file contains parse errors: %s", declPGF.URI)
}

// find the struct type declaration
pos := fieldInfo.Named.Obj().Pos()
endPos := pos + token.Pos(len(fieldInfo.Named.Obj().Name()))
curIdent, ok := declPGF.Cursor.FindPos(pos, endPos)
if !ok {
return nil, nil, fmt.Errorf("could not find identifier at position %v-%v", pos, endPos)
}

// Rest of the code remains the same
typeNode, ok := curIdent.NextSibling()
if !ok {
return nil, nil, fmt.Errorf("could not find type specification")
}

structType, ok := typeNode.Node().(*ast.StructType)
if !ok {
return nil, nil, fmt.Errorf("type at position %v is not a struct type", pos)
}
// Find metadata for the symbol's declaring package
// as we'll need its import mapping.
declMeta := findFileInDeps(snapshot, mp, declPGF.URI)
if declMeta == nil {
return nil, nil, bug.Errorf("can't find metadata for file %s among dependencies of %s", declPGF.URI, mp)
}

qual := newNamedImportQual(declPGF, snapshot, declMeta, fieldInfo.Named.Obj(), func(imp newImport) { /* discard */ })

// find the position to insert the new field (end of struct fields)
insertPos := structType.Fields.Closing - 1
if insertPos == structType.Fields.Opening {
// struct has no fields yet
insertPos = structType.Fields.Closing
_, err = declPGF.Mapper.PosRange(declPGF.Tok, insertPos, insertPos)
if err != nil {
return nil, nil, err
}
}

var buf bytes.Buffer
if err := fieldInfo.Emit(&buf, qual); err != nil {
return nil, nil, err
}

return fieldInfo.Fset, &analysis.SuggestedFix{
Message: fmt.Sprintf("Add field %s to struct %s", fieldInfo.Expr.Sel.Name, fieldInfo.Named.Obj().Name()),
TextEdits: []analysis.TextEdit{{
Pos: insertPos,
End: insertPos,
NewText: buf.Bytes(),
}},
}, nil
}

type newImport struct {
name string
importPath string
}

func newNamedImportQual(declPGF *parsego.File, snapshot *cache.Snapshot, declMeta *metadata.Package, sym types.Object, newImportHandler func(imp newImport)) func(*types.Package) string {
// Build import environment for the declaring file.
// (typesinternal.FileQualifier works only for complete
// import mappings, and requires types.)
importEnv := make(map[ImportPath]string) // value is local name
for _, imp := range declPGF.File.Imports {
importPath := metadata.UnquoteImportPath(imp)
var name string
if imp.Name != nil {
name = imp.Name.Name
if name == "_" {
continue
} else if name == "." {
name = "" // see types.Qualifier
}
} else {
// Use the correct name from the metadata of the imported
// package---not a guess based on the import path.
mp := snapshot.Metadata(declMeta.DepsByImpPath[importPath])
if mp == nil {
continue // can't happen?
}
name = string(mp.Name)
}
importEnv[importPath] = name // latest alias wins
}

// Create a package name qualifier that uses the
// locally appropriate imported package name.
// It records any needed new imports.
// TODO(adonovan): factor with golang.FormatVarType?
//
// Prior to CL 469155 this logic preserved any renaming
// imports from the file that declares the interface
// method--ostensibly the preferred name for imports of
// frequently renamed packages such as protobufs.
// Now we use the package's declared name. If this turns out
// to be a mistake, then use parseHeader(si.iface.Pos()).
//
return func(pkg *types.Package) string {
// TODO(adonovan): don't ignore vendor prefix.
//
// Ignore the current package import.
if pkg.Path() == sym.Pkg().Path() {
return ""
}

importPath := ImportPath(pkg.Path())
name, ok := importEnv[importPath]
if !ok {
// Insert new import using package's declared name.
//
// TODO(adonovan): resolve conflict between declared
// name and existing file-level (declPGF.File.Imports)
// or package-level (sym.Pkg.Scope) decls by
// generating a fresh name.
name = pkg.Name()
importEnv[importPath] = name
new := newImport{importPath: string(importPath)}
// For clarity, use a renaming import whenever the
// local name does not match the path's last segment.
if name != pathpkg.Base(trimVersionSuffix(new.importPath)) {
new.name = name
}
newImportHandler(new)
}
return name
}
}
2 changes: 1 addition & 1 deletion gopls/internal/golang/stubmethods/stubcalledfunc.go
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ func GetCallStubInfo(fset *token.FileSet, info *types.Info, pgf *parsego.File, s
// If recvExpr is a package name, compiler error would be
// e.g., "undefined: http.bar", thus will not hit this code path.
recvExpr := s.X
recvType, pointer := concreteType(recvExpr, info)
recvType, pointer := concreteType(info, recvExpr)

if recvType == nil || recvType.Obj().Pkg() == nil {
return nil
85 changes: 80 additions & 5 deletions gopls/internal/golang/stubmethods/stubmethods.go
Original file line number Diff line number Diff line change
@@ -225,7 +225,7 @@ func fromCallExpr(fset *token.FileSet, info *types.Info, pos token.Pos, call *as
return nil
}

concType, pointer := concreteType(arg, info)
concType, pointer := concreteType(info, arg)
if concType == nil || concType.Obj().Pkg() == nil {
return nil
}
@@ -279,7 +279,7 @@ func fromReturnStmt(fset *token.FileSet, info *types.Info, pos token.Pos, path [
return nil, fmt.Errorf("pos %d not within return statement bounds: [%d-%d]", pos, ret.Pos(), ret.End())
}

concType, pointer := concreteType(ret.Results[returnIdx], info)
concType, pointer := concreteType(info, ret.Results[returnIdx])
if concType == nil || concType.Obj().Pkg() == nil {
return nil, nil // result is not a named or *named or alias thereof
}
@@ -337,7 +337,7 @@ func fromValueSpec(fset *token.FileSet, info *types.Info, spec *ast.ValueSpec, p
ifaceNode = call.Fun
rhs = call.Args[0]
}
concType, pointer := concreteType(rhs, info)
concType, pointer := concreteType(info, rhs)
if concType == nil || concType.Obj().Pkg() == nil {
return nil
}
@@ -392,7 +392,7 @@ func fromAssignStmt(fset *token.FileSet, info *types.Info, assign *ast.AssignStm
if ifaceObj == nil {
return nil
}
concType, pointer := concreteType(rhs, info)
concType, pointer := concreteType(info, rhs)
if concType == nil || concType.Obj().Pkg() == nil {
return nil
}
@@ -441,7 +441,7 @@ func ifaceObjFromType(t types.Type) *types.TypeName {
// method will return a nil *types.Named. The second return parameter
// is a boolean that indicates whether the concreteType was defined as a
// pointer or value.
func concreteType(e ast.Expr, info *types.Info) (*types.Named, bool) {
func concreteType(info *types.Info, e ast.Expr) (*types.Named, bool) {
tv, ok := info.Types[e]
if !ok {
return nil, false
@@ -457,3 +457,78 @@ func concreteType(e ast.Expr, info *types.Info) (*types.Named, bool) {
}
return named, isPtr
}

// GetFieldStubInfo finds innermost enclosing selector x.f where x is a named struct type or a pointer to a struct type.
func GetFieldStubInfo(fset *token.FileSet, info *types.Info, pgf *parsego.File, start, end token.Pos) *StructFieldInfo {
path, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
for _, node := range path {
s, ok := node.(*ast.SelectorExpr)
if ok {
// If recvExpr is a package name, compiler error would be
// e.g., "undefined: http.bar", thus will not hit this code path.
recvExpr := s.X
recvNamed, _ := concreteType(info, recvExpr)

if recvNamed == nil || recvNamed.Obj().Pkg() == nil {
return nil
}

structType, ok := recvNamed.Underlying().(*types.Struct)
if !ok {
break
}

// Have: x.f where x has a named struct type.
return &StructFieldInfo{
Fset: fset,
Expr: s,
Named: recvNamed,
info: info,
path: path,
structType: structType,
}
}
}

return nil
}

// StructFieldInfo describes f field in x.f where x has a named struct type
type StructFieldInfo struct {
// Fset is a file set to provide a file where the struct field is accessed
Fset *token.FileSet
// Expr is a selector expression
Expr *ast.SelectorExpr
// Named is a selected struct type
Named *types.Named

info *types.Info
// path is a node path to SelectorExpr
path []ast.Node
// structType is an underlying struct type, makes sure the receiver is a struct
structType *types.Struct
}

// Emit writes to out the missing field based on type info.
func (si *StructFieldInfo) Emit(out *bytes.Buffer, qual types.Qualifier) error {
if si.Expr == nil || si.Expr.Sel == nil {
return fmt.Errorf("invalid selector expression")
}

// Get types from context at the selector expression position
typesFromContext := typesutil.TypesFromContext(si.info, si.path, si.Expr.Pos())

// Default to any if we couldn't determine the type from context
var fieldType types.Type
if len(typesFromContext) > 0 {
fieldType = typesFromContext[0]
} else {
fieldType = types.Universe.Lookup("any").Type()
}

fmt.Fprintf(out, "\n\t%s %s", si.Expr.Sel.Name, types.TypeString(fieldType, qual))
if si.structType.NumFields() == 0 {
out.WriteByte('\n')
}
return nil
}
132 changes: 132 additions & 0 deletions gopls/internal/test/marker/testdata/quickfix/struct_field.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
This test checks the 'Declare missing struct field' quick fix.

-- go.mod --
module module

-- package.go --
package field

import "module/another"
import alias "module/second"

type Struct struct{}

type AnotherStruct struct {
Chan <-chan Struct
}

func do() {
a := AnotherStruct{}

s := Struct{}
s.intField = 42 //@quickfix("intField", re"has no field or method", value_field)

var mp map[string]string = map[string]string{"key": "value4"}
s.mapField = mp //@quickfix("mapField", re"has no field or method", var_field)

s.chanField = a.Chan //@quickfix("chanField", re"has no field or method", another_struct_field)
s.sliceField = make([]map[string]Struct, 0) //@quickfix("sliceField", re"has no field or method", make_field)
s.sliceIntField = []int{1, 2} //@quickfix("sliceIntField", re"has no field or method", slice_int_field)
s.another = another.Another{} //@quickfix("another", re"has no field or method", another_package)
s.alias = alias.Second{} //@quickfix("alias", re"has no field or method", alias)
var al alias.Second
s.implicitAlias = al //@quickfix("implicitAlias", re"has no field or method", implicit_alias)
s.imported = alias.Second{}.Imported //@quickfix("imported", re"has no field or method", auto_import)
s.newField = new(Struct) //@quickfix("newField", re"has no field or method", new_field)
s.pointerField = &Struct{} //@quickfix("pointerField", re"has no field or method", pointer)
var p *Struct
s.derefedField = *p //@quickfix("derefedField", re"has no field or method", deref)

a.properlyFormattedField = 42 //@quickfix("properlyFormattedField", re"has no field or method", formatted)
}
-- another/another.go --
package another

type Another struct {}
-- second/second.go --
package second

import "module/imported"

type Second struct{
Imported imported.Imported
}
-- imported/imported.go --
package imported

type Imported struct{}
-- @value_field/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ intField int
+}
-- @var_field/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ mapField map[string]string
+}
-- @another_struct_field/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ chanField <-chan Struct
+}
-- @slice_int_field/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ sliceIntField []int
+}
-- @make_field/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ sliceField []map[string]Struct
+}
-- @another_package/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ another another.Another
+}
-- @alias/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ alias alias.Second
+}
-- @implicit_alias/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ implicitAlias alias.Second
+}
-- @auto_import/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ imported imported.Imported
+}
-- @new_field/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ newField *Struct
+}
-- @pointer/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ pointerField *Struct
+}
-- @deref/package.go --
@@ -6 +6,3 @@
-type Struct struct{}
+type Struct struct{
+ derefedField Struct
+}
-- @formatted/package.go --
@@ -10 +10 @@
+ properlyFormattedField int
62 changes: 43 additions & 19 deletions gopls/internal/util/typesutil/typesutil.go
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ import (
"go/token"
"go/types"
"strings"

"golang.org/x/tools/gopls/internal/util/astutil"
)

// FormatTypeParams turns TypeParamList into its Go representation, such as:
@@ -62,26 +64,12 @@ func TypesFromContext(info *types.Info, path []ast.Node, pos token.Pos) []types.

switch parent := parent.(type) {
case *ast.AssignStmt:
// Append all lhs's type
if len(parent.Rhs) == 1 {
for _, lhs := range parent.Lhs {
t := info.TypeOf(lhs)
typs = append(typs, validType(t))
}
break
}
// Lhs and Rhs counts do not match, give up
if len(parent.Lhs) != len(parent.Rhs) {
break
}
// Append corresponding index of lhs's type
for i, rhs := range parent.Rhs {
if rhs.Pos() <= pos && pos <= rhs.End() {
t := info.TypeOf(parent.Lhs[i])
typs = append(typs, validType(t))
break
}
right := pos > parent.TokPos
expr, opposites := parent.Lhs, parent.Rhs
if right {
expr, opposites = opposites, expr
}
typs = append(typs, typeFromExprAssignExpr(expr, opposites, info, pos, validType)...)
case *ast.ValueSpec:
if len(parent.Values) == 1 {
for _, lhs := range parent.Names {
@@ -185,6 +173,14 @@ func TypesFromContext(info *types.Info, path []ast.Node, pos token.Pos) []types.
t := info.TypeOf(parent.X)
typs = append(typs, validType(t))
}
case *ast.SelectorExpr:
for _, n := range path {
assign, ok := n.(*ast.AssignStmt)
if ok {
return TypesFromContext(info, path[1:], assign.Pos())
}
}

default:
// TODO: support other kinds of "holes" as the need arises.
}
@@ -233,3 +229,31 @@ func EnclosingSignature(path []ast.Node, info *types.Info) *types.Signature {
}
return nil
}

// typeFromExprAssignExpr extracts a type from a given expression
// f.x = v
// where v - a value which type the function extracts
func typeFromExprAssignExpr(exprs, opposites []ast.Expr, info *types.Info, pos token.Pos, validType func(t types.Type) types.Type) []types.Type {
var typs []types.Type
// Append all lhs's type
if len(exprs) == 1 {
for i := range opposites {
t := info.TypeOf(opposites[i])
typs = append(typs, validType(t))
}
return typs
}
// Lhs and Rhs counts do not match, give up
if len(opposites) != len(exprs) {
return nil
}
// Append corresponding index of lhs's type
for i := range exprs {
if astutil.NodeContains(exprs[i], pos) {
t := info.TypeOf(opposites[i])
return []types.Type{validType(t)}
}
}

return nil
}