Skip to content

Commit

Permalink
x/tools/gopls: implement struct field generation quickfix
Browse files Browse the repository at this point in the history
  • Loading branch information
dennypenta committed Dec 3, 2024
1 parent 33ea340 commit 054921c
Show file tree
Hide file tree
Showing 7 changed files with 496 additions and 3 deletions.
29 changes: 29 additions & 0 deletions gopls/doc/features/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,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 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
10 changes: 9 additions & 1 deletion gopls/doc/release/v0.17.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,12 @@ 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 utilizes the
[gotests](https://github.com/cweill/gotests) tool.
[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.
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 @@ -322,14 +323,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"):
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)
}
}

// "undeclared name: x" or "undefined: x" compiler error.
Expand All @@ -348,6 +358,75 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
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 := typesutil.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 @@ -68,6 +68,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
Expand Down Expand Up @@ -113,6 +114,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 {
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

0 comments on commit 054921c

Please sign in to comment.