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 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions gopls/doc/features/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,34 @@ func (f Foo) bar(s string, i int) string {
}
```

### `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 accessing type or assigning a designated value.

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:
Expand Down
8 changes: 8 additions & 0 deletions gopls/doc/release/v0.17.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,11 @@ from the context of the call.
The new `yield` analyzer detects mistakes using the `yield` function
in a Go 1.23 iterator, such as failure to check its boolean result and
break out of a loop.

## 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.
83 changes: 81 additions & 2 deletions gopls/internal/golang/codeaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package golang

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -320,21 +321,99 @@ 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"):
path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end)

// Offer a "Declare missing method T.f" code action if a CallStubInfo found.
// See [stubMissingCalledFunctionFixer] for command implementation.
si := stubmethods.GetCallStubInfo(req.pkg.FileSet(), info, path, start)
if si != nil {
msg := fmt.Sprintf("Declare missing method %s.%s", si.Receiver.Obj().Name(), si.MethodName)
req.addApplyFixAction(msg, fixMissingCalledFunction, req.loc)
} else {
// Offer a "Declare missing field T.f" code action.
// See [stubMissingStructFieldFixer] for command implementation.
fi := GetFieldStubInfo(req.pkg.FileSet(), info, path)
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)
}
}
}
}

return nil
}

func GetFieldStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node) *StructFieldInfo {
for _, node := range path {
n, ok := node.(*ast.SelectorExpr)
if !ok {
continue
}
tv, ok := info.Types[n.X]
if !ok {
break
}

named, ok := tv.Type.(*types.Named)
if !ok {
break
}

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

return &StructFieldInfo{
Fset: fset,
Expr: n,
Struct: structType,
Named: named,
Info: info,
Path: path,
}
}

return nil
}

type StructFieldInfo struct {
Fset *token.FileSet
Expr *ast.SelectorExpr
Struct *types.Struct
Named *types.Named
Info *types.Info
Path []ast.Node
}

// 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 := stubmethods.TypesFromContext(si.Info, si.Path, si.Expr.Pos())

// Default to interface{} if we couldn't determine the type from context
var fieldType types.Type
if len(typesFromContext) > 0 && typesFromContext[0] != nil {
fieldType = typesFromContext[0]
} else {
// Create a new interface{} type
fieldType = types.NewInterfaceType(nil, nil)
}

tpl := "\n\t%s %s"
if si.Struct.NumFields() == 0 {
tpl += "\n"
}
fmt.Fprintf(out, tpl, si.Expr.Sel.Name, types.TypeString(fieldType, qual))
return nil
}

// allImportsFixesResult is the result of a lazy call to allImportsFixes.
// It implements the codeActionsRequest lazyInit interface.
type allImportsFixesResult struct {
Expand Down
2 changes: 2 additions & 0 deletions gopls/internal/golang/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const (
fixJoinLines = "join_lines"
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
Expand Down Expand Up @@ -112,6 +113,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file
fixJoinLines: singleFile(joinLines),
fixMissingInterfaceMethods: stubMissingInterfaceMethodsFixer,
fixMissingCalledFunction: stubMissingCalledFunctionFixer,
fixMissingStructField: stubMissingStructFieldFixer,
}
fixer, ok := fixers[fix]
if !ok {
Expand Down
76 changes: 76 additions & 0 deletions gopls/internal/golang/stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"context"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
Expand Down Expand Up @@ -51,6 +52,18 @@ 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) {
nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
fi := GetFieldStubInfo(pkg.FileSet(), pkg.TypesInfo(), nodes)
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.
Expand Down Expand Up @@ -238,3 +251,66 @@ func trimVersionSuffix(path string) string {
}
return path
}

func insertStructField(ctx context.Context, snapshot *cache.Snapshot, meta *metadata.Package, fieldInfo *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
var structType *ast.StructType
ast.Inspect(declPGF.File, func(n ast.Node) bool {
if typeSpec, ok := n.(*ast.TypeSpec); ok {
if typeSpec.Name.Name == fieldInfo.Named.Obj().Name() {
if st, ok := typeSpec.Type.(*ast.StructType); ok {
structType = st
return false
}
}
}
return true
})

if structType == nil {
return nil, nil, fmt.Errorf("could not find struct definition")
}

// 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
}

var buf bytes.Buffer
if err := fieldInfo.Emit(&buf, types.RelativeTo(fieldInfo.Named.Obj().Pkg())); err != nil {
return nil, nil, err
}

_, err = declPGF.Mapper.PosRange(declPGF.Tok, insertPos, insertPos)
if err != nil {
return nil, nil, err
}

textEdit := analysis.TextEdit{
Pos: insertPos,
End: insertPos,
NewText: []byte(buf.String()),
}

fix := &analysis.SuggestedFix{
Message: fmt.Sprintf("Add field %s to struct %s", fieldInfo.Expr.Sel.Name, fieldInfo.Named.Obj().Name()),
TextEdits: []analysis.TextEdit{textEdit},
}

return fieldInfo.Fset, fix, nil
}
Loading