Skip to content

Commit 1675eb9

Browse files
committed
x/tools/gopls: implement struct field generation quickfix
1 parent e426616 commit 1675eb9

File tree

7 files changed

+510
-4
lines changed

7 files changed

+510
-4
lines changed

gopls/doc/features/diagnostics.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,35 @@ func doSomething(i int) string {
272272
panic("unimplemented")
273273
}
274274
```
275+
276+
### `StubMissingStructField`: Declare missing field T.f
277+
278+
When you attempt to access a field on a type that does not have the field,
279+
the compiler will report an error such as "type X has no field or method Y".
280+
In this scenario, gopls now offers a quick fix to generate a stub declaration of
281+
the missing field, inferring its type from the accessing type or assigning a designated value.
282+
283+
Consider the following code where `Foo` does not have a field `bar`:
284+
285+
```go
286+
type Foo struct{}
287+
288+
func main() {
289+
var s string
290+
f := Foo{}
291+
s = f.bar // error: f.bar undefined (type Foo has no field or method bar)
292+
}
293+
```
294+
295+
Gopls will offer a quick fix, "Declare missing field Foo.bar".
296+
When invoked, it creates the following declaration:
297+
298+
```go
299+
type Foo struct{
300+
bar string
301+
}
302+
```
303+
275304
<!--
276305
277306
dorky details and deletia:

gopls/doc/release/v0.17.0.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ upgrading a module dependency.
3636

3737
## Go command compatibility: the 2 most recent major Go versions
3838

39-
The [email protected] releases will be the final versions of gopls to nominally
39+
The <[email protected]> releases will be the final versions of gopls to nominally
4040
support integrating with more than the 2 most recent Go releases. In the past,
4141
we implied "best effort" support for up to 4 versions, though in practice we
4242
did not have resources to fix bugs that were present only with older Go
@@ -169,7 +169,7 @@ constructor of the type of each symbol:
169169
`interface`, `struct`, `signature`, `pointer`, `array`, `map`, `slice`, `chan`, `string`, `number`, `bool`, and `invalid`.
170170
Editors may use this for syntax coloring.
171171

172-
## SignatureHelp for ident and values.
172+
## SignatureHelp for ident and values
173173

174174
Now, function signature help can be used on any identifier with a function
175175
signature, not just within the parentheses of a function being called.
@@ -196,3 +196,24 @@ causing `Add` to race with `Wait`.
196196
(This check is equivalent to
197197
[staticcheck's SA2000](https://staticcheck.dev/docs/checks#SA2000),
198198
but is enabled by default.)
199+
200+
## Add test for function or method
201+
202+
If the selected chunk of code is part of a function or method declaration F,
203+
gopls will offer the "Add test for F" code action, which adds a new test for the
204+
selected function in the corresponding `_test.go` file. The generated test takes
205+
into account its signature, including input parameters and results.
206+
207+
Since this feature is implemented by the server (gopls), it is compatible with
208+
all LSP-compliant editors. VS Code users may continue to use the client-side
209+
`Go: Generate Unit Tests For file/function/package` command which utilizes the
210+
[gotests](https://github.com/cweill/gotests) tool.
211+
212+
## Generate missing struct field from access
213+
214+
When you attempt to access a field on a type that does not have the field,
215+
the compiler will report an error like “type X has no field or method Y”.
216+
Gopls now offers a new code action, “Declare missing field of T.f”,
217+
where T is the concrete type and f is the undefined field.
218+
The stub field's signature is inferred
219+
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"
@@ -328,14 +329,23 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
328329
}
329330

330331
// "type X has no field or method Y" compiler error.
331-
// Offer a "Declare missing method T.f" code action.
332-
// See [stubMissingCalledFunctionFixer] for command implementation.
333332
case strings.Contains(msg, "has no field or method"):
334333
path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end)
334+
335+
// Offer a "Declare missing method T.f" code action if a CallStubInfo found.
336+
// See [stubMissingCalledFunctionFixer] for command implementation.
335337
si := stubmethods.GetCallStubInfo(req.pkg.FileSet(), info, path, start)
336338
if si != nil {
337339
msg := fmt.Sprintf("Declare missing method %s.%s", si.Receiver.Obj().Name(), si.MethodName)
338340
req.addApplyFixAction(msg, fixMissingCalledFunction, req.loc)
341+
} else {
342+
// Offer a "Declare missing field T.f" code action.
343+
// See [stubMissingStructFieldFixer] for command implementation.
344+
fi := GetFieldStubInfo(req.pkg.FileSet(), info, path)
345+
if fi != nil {
346+
msg := fmt.Sprintf("Declare missing struct field %s.%s", fi.Named.Obj().Name(), fi.Expr.Sel.Name)
347+
req.addApplyFixAction(msg, fixMissingStructField, req.loc)
348+
}
339349
}
340350

341351
// "undeclared name: X" or "undefined: X" compiler error.
@@ -354,6 +364,75 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
354364
return nil
355365
}
356366

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

gopls/internal/golang/fix.go

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

7475
// ApplyFix applies the specified kind of suggested fix to the given
@@ -115,6 +116,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file
115116
fixCreateUndeclared: singleFile(createUndeclared),
116117
fixMissingInterfaceMethods: stubMissingInterfaceMethodsFixer,
117118
fixMissingCalledFunction: stubMissingCalledFunctionFixer,
119+
fixMissingStructField: stubMissingStructFieldFixer,
118120
}
119121
fixer, ok := fixers[fix]
120122
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+
}

0 commit comments

Comments
 (0)