Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5f30384

Browse files
committedDec 3, 2024·
x/tools/gopls: implement struct field generation quickfix
1 parent 4296223 commit 5f30384

File tree

7 files changed

+496
-3
lines changed

7 files changed

+496
-3
lines changed
 

‎gopls/doc/features/diagnostics.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,35 @@ func doSomething(i int) string {
248248
panic("unimplemented")
249249
}
250250
```
251+
252+
### `StubMissingStructField`: Declare missing field T.f
253+
254+
When you attempt to access a field on a type that does not have the field,
255+
the compiler will report an error such as "type X has no field or method Y".
256+
In this scenario, gopls now offers a quick fix to generate a stub declaration of
257+
the missing field, inferring its type from the accessing type or assigning a designated value.
258+
259+
Consider the following code where `Foo` does not have a field `bar`:
260+
261+
```go
262+
type Foo struct{}
263+
264+
func main() {
265+
var s string
266+
f := Foo{}
267+
s = f.bar // error: f.bar undefined (type Foo has no field or method bar)
268+
}
269+
```
270+
271+
Gopls will offer a quick fix, "Declare missing field Foo.bar".
272+
When invoked, it creates the following declaration:
273+
274+
```go
275+
type Foo struct{
276+
bar string
277+
}
278+
```
279+
251280
<!--
252281
253282
dorky details and deletia:

‎gopls/doc/release/v0.17.0.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,12 @@ into account its signature, including input parameters and results.
113113
Since this feature is implemented by the server (gopls), it is compatible with
114114
all LSP-compliant editors. VS Code users may continue to use the client-side
115115
`Go: Generate Unit Tests For file/function/package` command which utilizes the
116-
[gotests](https://github.com/cweill/gotests) tool.
116+
[gotests](https://github.com/cweill/gotests) tool.
117+
118+
## Generate missing struct field from access
119+
When you attempt to access a field on a type that does not have the field,
120+
the compiler will report an error like “type X has no field or method Y”.
121+
Gopls now offers a new code action, “Declare missing field of T.f”,
122+
where T is the concrete type and f is the undefined field.
123+
The stub field's signature is inferred
124+
from the context of the access.

‎gopls/internal/golang/codeaction.go

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package golang
66

77
import (
8+
"bytes"
89
"context"
910
"encoding/json"
1011
"fmt"
@@ -322,14 +323,23 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
322323
}
323324

324325
// "type X has no field or method Y" compiler error.
325-
// Offer a "Declare missing method T.f" code action.
326-
// See [stubMissingCalledFunctionFixer] for command implementation.
327326
case strings.Contains(msg, "has no field or method"):
328327
path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end)
328+
329+
// Offer a "Declare missing method T.f" code action if a CallStubInfo found.
330+
// See [stubMissingCalledFunctionFixer] for command implementation.
329331
si := stubmethods.GetCallStubInfo(req.pkg.FileSet(), info, path, start)
330332
if si != nil {
331333
msg := fmt.Sprintf("Declare missing method %s.%s", si.Receiver.Obj().Name(), si.MethodName)
332334
req.addApplyFixAction(msg, fixMissingCalledFunction, req.loc)
335+
} else {
336+
// Offer a "Declare missing field T.f" code action.
337+
// See [stubMissingStructFieldFixer] for command implementation.
338+
fi := GetFieldStubInfo(req.pkg.FileSet(), info, path)
339+
if fi != nil {
340+
msg := fmt.Sprintf("Declare missing struct field %s.%s", fi.Named.Obj().Name(), fi.Expr.Sel.Name)
341+
req.addApplyFixAction(msg, fixMissingStructField, req.loc)
342+
}
333343
}
334344

335345
// "undeclared name: x" or "undefined: x" compiler error.
@@ -348,6 +358,75 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
348358
return nil
349359
}
350360

361+
func GetFieldStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node) *StructFieldInfo {
362+
for _, node := range path {
363+
n, ok := node.(*ast.SelectorExpr)
364+
if !ok {
365+
continue
366+
}
367+
tv, ok := info.Types[n.X]
368+
if !ok {
369+
break
370+
}
371+
372+
named, ok := tv.Type.(*types.Named)
373+
if !ok {
374+
break
375+
}
376+
377+
structType, ok := named.Underlying().(*types.Struct)
378+
if !ok {
379+
break
380+
}
381+
382+
return &StructFieldInfo{
383+
Fset: fset,
384+
Expr: n,
385+
Struct: structType,
386+
Named: named,
387+
Info: info,
388+
Path: path,
389+
}
390+
}
391+
392+
return nil
393+
}
394+
395+
type StructFieldInfo struct {
396+
Fset *token.FileSet
397+
Expr *ast.SelectorExpr
398+
Struct *types.Struct
399+
Named *types.Named
400+
Info *types.Info
401+
Path []ast.Node
402+
}
403+
404+
// Emit writes to out the missing field based on type info.
405+
func (si *StructFieldInfo) Emit(out *bytes.Buffer, qual types.Qualifier) error {
406+
if si.Expr == nil || si.Expr.Sel == nil {
407+
return fmt.Errorf("invalid selector expression")
408+
}
409+
410+
// Get types from context at the selector expression position
411+
typesFromContext := typesutil.TypesFromContext(si.Info, si.Path, si.Expr.Pos())
412+
413+
// Default to interface{} if we couldn't determine the type from context
414+
var fieldType types.Type
415+
if len(typesFromContext) > 0 && typesFromContext[0] != nil {
416+
fieldType = typesFromContext[0]
417+
} else {
418+
// Create a new interface{} type
419+
fieldType = types.NewInterfaceType(nil, nil)
420+
}
421+
422+
tpl := "\n\t%s %s"
423+
if si.Struct.NumFields() == 0 {
424+
tpl += "\n"
425+
}
426+
fmt.Fprintf(out, tpl, si.Expr.Sel.Name, types.TypeString(fieldType, qual))
427+
return nil
428+
}
429+
351430
// allImportsFixesResult is the result of a lazy call to allImportsFixes.
352431
// It implements the codeActionsRequest lazyInit interface.
353432
type allImportsFixesResult struct {

‎gopls/internal/golang/fix.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const (
6868
fixCreateUndeclared = "create_undeclared"
6969
fixMissingInterfaceMethods = "stub_missing_interface_method"
7070
fixMissingCalledFunction = "stub_missing_called_function"
71+
fixMissingStructField = "stub_missing_struct_field"
7172
)
7273

7374
// ApplyFix applies the specified kind of suggested fix to the given
@@ -113,6 +114,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file
113114
fixCreateUndeclared: singleFile(CreateUndeclared),
114115
fixMissingInterfaceMethods: stubMissingInterfaceMethodsFixer,
115116
fixMissingCalledFunction: stubMissingCalledFunctionFixer,
117+
fixMissingStructField: stubMissingStructFieldFixer,
116118
}
117119
fixer, ok := fixers[fix]
118120
if !ok {

‎gopls/internal/golang/stub.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"context"
1010
"fmt"
11+
"go/ast"
1112
"go/format"
1213
"go/parser"
1314
"go/token"
@@ -51,6 +52,18 @@ func stubMissingCalledFunctionFixer(ctx context.Context, snapshot *cache.Snapsho
5152
return insertDeclsAfter(ctx, snapshot, pkg.Metadata(), si.Fset, si.After, si.Emit)
5253
}
5354

55+
// stubMissingStructFieldFixer returns a suggested fix to declare the missing
56+
// field that the user may want to generate based on SelectorExpr
57+
// at the cursor position.
58+
func stubMissingStructFieldFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
59+
nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
60+
fi := GetFieldStubInfo(pkg.FileSet(), pkg.TypesInfo(), nodes)
61+
if fi == nil {
62+
return nil, nil, fmt.Errorf("invalid type request")
63+
}
64+
return insertStructField(ctx, snapshot, pkg.Metadata(), fi)
65+
}
66+
5467
// An emitter writes new top-level declarations into an existing
5568
// file. References to symbols should be qualified using qual, which
5669
// respects the local import environment.
@@ -238,3 +251,66 @@ func trimVersionSuffix(path string) string {
238251
}
239252
return path
240253
}
254+
255+
func insertStructField(ctx context.Context, snapshot *cache.Snapshot, meta *metadata.Package, fieldInfo *StructFieldInfo) (*token.FileSet, *analysis.SuggestedFix, error) {
256+
if fieldInfo == nil {
257+
return nil, nil, fmt.Errorf("no field info provided")
258+
}
259+
260+
// get the file containing the struct definition using the position
261+
declPGF, _, err := parseFull(ctx, snapshot, fieldInfo.Fset, fieldInfo.Named.Obj().Pos())
262+
if err != nil {
263+
return nil, nil, fmt.Errorf("failed to parse file declaring struct: %w", err)
264+
}
265+
if declPGF.Fixed() {
266+
return nil, nil, fmt.Errorf("file contains parse errors: %s", declPGF.URI)
267+
}
268+
269+
// find the struct type declaration
270+
var structType *ast.StructType
271+
ast.Inspect(declPGF.File, func(n ast.Node) bool {
272+
if typeSpec, ok := n.(*ast.TypeSpec); ok {
273+
if typeSpec.Name.Name == fieldInfo.Named.Obj().Name() {
274+
if st, ok := typeSpec.Type.(*ast.StructType); ok {
275+
structType = st
276+
return false
277+
}
278+
}
279+
}
280+
return true
281+
})
282+
283+
if structType == nil {
284+
return nil, nil, fmt.Errorf("could not find struct definition")
285+
}
286+
287+
// find the position to insert the new field (end of struct fields)
288+
insertPos := structType.Fields.Closing - 1
289+
if insertPos == structType.Fields.Opening {
290+
// struct has no fields yet
291+
insertPos = structType.Fields.Closing
292+
}
293+
294+
var buf bytes.Buffer
295+
if err := fieldInfo.Emit(&buf, types.RelativeTo(fieldInfo.Named.Obj().Pkg())); err != nil {
296+
return nil, nil, err
297+
}
298+
299+
_, err = declPGF.Mapper.PosRange(declPGF.Tok, insertPos, insertPos)
300+
if err != nil {
301+
return nil, nil, err
302+
}
303+
304+
textEdit := analysis.TextEdit{
305+
Pos: insertPos,
306+
End: insertPos,
307+
NewText: []byte(buf.String()),
308+
}
309+
310+
fix := &analysis.SuggestedFix{
311+
Message: fmt.Sprintf("Add field %s to struct %s", fieldInfo.Expr.Sel.Name, fieldInfo.Named.Obj().Name()),
312+
TextEdits: []analysis.TextEdit{textEdit},
313+
}
314+
315+
return fieldInfo.Fset, fix, nil
316+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
This test checks the 'Declare missing struct field' quick fix.
2+
3+
-- go.mod --
4+
module module
5+
6+
-- package.go --
7+
package field
8+
9+
import "module/another"
10+
import alias "module/second"
11+
12+
type Struct struct{}
13+
14+
type AnotherStruct struct {
15+
Chan <-chan Struct
16+
}
17+
18+
func do() {
19+
a := AnotherStruct{}
20+
21+
s := Struct{}
22+
s.intField = 42 //@quickfix("intField", re"has no field or method", value_field)
23+
24+
var mp map[string]string = map[string]string{"key": "value4"}
25+
s.mapField = mp //@quickfix("mapField", re"has no field or method", var_field)
26+
27+
s.chanField = a.Chan //@quickfix("chanField", re"has no field or method", another_struct_field)
28+
s.sliceField = make([]map[string]Struct, 0) //@quickfix("sliceField", re"has no field or method", make_field)
29+
s.sliceIntField = []int{1, 2} //@quickfix("sliceIntField", re"has no field or method", slice_int_field)
30+
s.another = another.Another{} //@quickfix("another", re"has no field or method", another_package)
31+
s.alias = alias.Second{} //@quickfix("alias", re"has no field or method", alias)
32+
var al alias.Second
33+
s.implicitAlias = al //@quickfix("implicitAlias", re"has no field or method", implicit_alias)
34+
s.imported = alias.Second{}.Imported //@quickfix("imported", re"has no field or method", auto_import)
35+
s.newField = new(Struct) //@quickfix("newField", re"has no field or method", new_field)
36+
s.pointerField = &Struct{} //@quickfix("pointerField", re"has no field or method", pointer)
37+
var p *Struct
38+
s.derefedField = *p //@quickfix("derefedField", re"has no field or method", deref)
39+
40+
a.properlyFormattedField = 42 //@quickfix("properlyFormattedField", re"has no field or method", formatted)
41+
}
42+
-- another/another.go --
43+
package another
44+
45+
type Another struct {}
46+
-- second/second.go --
47+
package second
48+
49+
import "module/imported"
50+
51+
type Second struct{
52+
Imported imported.Imported
53+
}
54+
-- imported/imported.go --
55+
package imported
56+
57+
type Imported struct{}
58+
-- @value_field/package.go --
59+
@@ -6 +6,3 @@
60+
-type Struct struct{}
61+
+type Struct struct{
62+
+ intField int
63+
+}
64+
-- @var_field/package.go --
65+
@@ -6 +6,3 @@
66+
-type Struct struct{}
67+
+type Struct struct{
68+
+ mapField map[string]string
69+
+}
70+
-- @another_struct_field/package.go --
71+
@@ -6 +6,3 @@
72+
-type Struct struct{}
73+
+type Struct struct{
74+
+ chanField <-chan Struct
75+
+}
76+
-- @slice_int_field/package.go --
77+
@@ -6 +6,3 @@
78+
-type Struct struct{}
79+
+type Struct struct{
80+
+ sliceIntField []int
81+
+}
82+
-- @make_field/package.go --
83+
@@ -6 +6,3 @@
84+
-type Struct struct{}
85+
+type Struct struct{
86+
+ sliceField []map[string]Struct
87+
+}
88+
-- @another_package/package.go --
89+
@@ -6 +6,3 @@
90+
-type Struct struct{}
91+
+type Struct struct{
92+
+ another another.Another
93+
+}
94+
-- @alias/package.go --
95+
@@ -6 +6,3 @@
96+
-type Struct struct{}
97+
+type Struct struct{
98+
+ alias alias.Second
99+
+}
100+
-- @implicit_alias/package.go --
101+
@@ -6 +6,3 @@
102+
-type Struct struct{}
103+
+type Struct struct{
104+
+ implicitAlias alias.Second
105+
+}
106+
-- @auto_import/package.go --
107+
@@ -6 +6,3 @@
108+
-type Struct struct{}
109+
+type Struct struct{
110+
+ imported imported.Imported
111+
+}
112+
-- @new_field/package.go --
113+
@@ -6 +6,3 @@
114+
-type Struct struct{}
115+
+type Struct struct{
116+
+ newField *Struct
117+
+}
118+
-- @pointer/package.go --
119+
@@ -6 +6,3 @@
120+
-type Struct struct{}
121+
+type Struct struct{
122+
+ pointerField *Struct
123+
+}
124+
-- @deref/package.go --
125+
@@ -6 +6,3 @@
126+
-type Struct struct{}
127+
+type Struct struct{
128+
+ derefedField Struct
129+
+}
130+
-- @formatted/package.go --
131+
@@ -10 +10 @@
132+
+ properlyFormattedField int

‎gopls/internal/util/typesutil/typesutil.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"go/ast"
1010
"go/token"
1111
"go/types"
12+
"strconv"
1213
"strings"
1314
)
1415

@@ -209,6 +210,120 @@ func TypesFromContext(info *types.Info, path []ast.Node, pos token.Pos) []types.
209210
t := info.TypeOf(parent.X)
210211
typs = append(typs, validType(t))
211212
}
213+
case *ast.SelectorExpr:
214+
for _, n := range path {
215+
assignExpr, ok := n.(*ast.AssignStmt)
216+
if ok {
217+
for _, rh := range assignExpr.Rhs {
218+
// basic types
219+
basicLit, ok := rh.(*ast.BasicLit)
220+
if ok {
221+
switch basicLit.Kind {
222+
case token.INT:
223+
typs = append(typs, types.Typ[types.Int])
224+
case token.FLOAT:
225+
typs = append(typs, types.Typ[types.Float64])
226+
case token.IMAG:
227+
typs = append(typs, types.Typ[types.Complex128])
228+
case token.STRING:
229+
typs = append(typs, types.Typ[types.String])
230+
case token.CHAR:
231+
typs = append(typs, types.Typ[types.Rune])
232+
}
233+
break
234+
}
235+
callExpr, ok := rh.(*ast.CallExpr)
236+
if ok {
237+
if ident, ok := callExpr.Fun.(*ast.Ident); ok && ident.Name == "make" && len(callExpr.Args) > 0 {
238+
arg := callExpr.Args[0]
239+
composite, ok := arg.(*ast.CompositeLit)
240+
if ok {
241+
t := typeFromExpr(info, path, composite)
242+
typs = append(typs, t)
243+
break
244+
}
245+
if t := info.TypeOf(arg); t != nil {
246+
typs = append(typs, validType(t))
247+
}
248+
}
249+
if ident, ok := callExpr.Fun.(*ast.Ident); ok && ident.Name == "new" && len(callExpr.Args) > 0 {
250+
arg := callExpr.Args[0]
251+
composite, ok := arg.(*ast.CompositeLit)
252+
if ok {
253+
t := typeFromExpr(info, path, composite)
254+
t = types.NewPointer(t)
255+
typs = append(typs, t)
256+
break
257+
}
258+
if t := info.TypeOf(arg); t != nil {
259+
if !containsInvalid(t) {
260+
t = types.Default(t)
261+
t = types.NewPointer(t)
262+
} else {
263+
t = anyType
264+
}
265+
typs = append(typs, t)
266+
}
267+
}
268+
break
269+
}
270+
// a variable
271+
ident, ok := rh.(*ast.Ident)
272+
if ok {
273+
if t := typeFromExpr(info, path, ident); t != nil {
274+
typs = append(typs, t)
275+
}
276+
break
277+
}
278+
279+
selectorExpr, ok := rh.(*ast.SelectorExpr)
280+
if ok {
281+
if t := typeFromExpr(info, path, selectorExpr.Sel); t != nil {
282+
typs = append(typs, t)
283+
}
284+
break
285+
}
286+
// composite
287+
composite, ok := rh.(*ast.CompositeLit)
288+
if ok {
289+
t := typeFromExpr(info, path, composite)
290+
typs = append(typs, t)
291+
break
292+
}
293+
// a pointer
294+
un, ok := rh.(*ast.UnaryExpr)
295+
if ok && un.Op == token.AND {
296+
composite, ok := un.X.(*ast.CompositeLit)
297+
if !ok {
298+
break
299+
}
300+
if t := info.TypeOf(composite); t != nil {
301+
if !containsInvalid(t) {
302+
t = types.Default(t)
303+
t = types.NewPointer(t)
304+
} else {
305+
t = anyType
306+
}
307+
typs = append(typs, t)
308+
}
309+
}
310+
starExpr, ok := rh.(*ast.StarExpr)
311+
if ok {
312+
ident, ok := starExpr.X.(*ast.Ident)
313+
if ok {
314+
if t := typeFromExpr(info, path, ident); t != nil {
315+
if pointer, ok := t.(*types.Pointer); ok {
316+
t = pointer.Elem()
317+
}
318+
typs = append(typs, t)
319+
}
320+
break
321+
}
322+
}
323+
}
324+
}
325+
}
326+
212327
default:
213328
// TODO: support other kinds of "holes" as the need arises.
214329
}
@@ -257,3 +372,55 @@ func EnclosingSignature(path []ast.Node, info *types.Info) *types.Signature {
257372
}
258373
return nil
259374
}
375+
376+
func typeFromExpr(info *types.Info, path []ast.Node, expr ast.Expr) types.Type {
377+
t := info.TypeOf(expr)
378+
if t == nil {
379+
return nil
380+
}
381+
382+
if !containsInvalid(t) {
383+
t = types.Default(t)
384+
if named, ok := t.(*types.Named); ok {
385+
if pkg := named.Obj().Pkg(); pkg != nil {
386+
// find the file in the path that contains this assignment
387+
var file *ast.File
388+
for _, n := range path {
389+
if f, ok := n.(*ast.File); ok {
390+
file = f
391+
break
392+
}
393+
}
394+
395+
if file != nil {
396+
// look for any import spec that imports this package
397+
var pkgName string
398+
for _, imp := range file.Imports {
399+
if path, _ := strconv.Unquote(imp.Path.Value); path == pkg.Path() {
400+
// use the alias if specified, otherwise use package name
401+
if imp.Name != nil {
402+
pkgName = imp.Name.Name
403+
} else {
404+
pkgName = pkg.Name()
405+
}
406+
break
407+
}
408+
}
409+
// fallback to package name if no import found
410+
if pkgName == "" {
411+
pkgName = pkg.Name()
412+
}
413+
414+
// create new package with the correct name (either alias or original)
415+
newPkg := types.NewPackage(pkgName, pkgName)
416+
newName := types.NewTypeName(named.Obj().Pos(), newPkg, named.Obj().Name(), nil)
417+
t = types.NewNamed(newName, named.Underlying(), nil)
418+
}
419+
}
420+
return t
421+
}
422+
} else {
423+
t = types.Universe.Lookup("any").Type()
424+
}
425+
return t
426+
}

0 commit comments

Comments
 (0)
Please sign in to comment.