internal/lsp/analysis/fillstruct: use AST nodes to fill struct

The current implementation writes strings to the diagnostic instead
of creating new AST nodes.

Change-Id: Ibb37a93a3c43fb74ec5dc687091601e055c6e1b4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/238198
Run-TryBot: Josh Baum <joshbaum@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Josh Baum 2020-06-15 15:20:17 -04:00
parent 6c6fd98e38
commit 0f592d2728
5 changed files with 58 additions and 29 deletions

View File

@ -11,8 +11,8 @@ import (
"fmt"
"go/ast"
"go/format"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
@ -23,7 +23,7 @@ import (
const Doc = `suggested input for incomplete struct initializations
This analyzer provides the appropriate zero values for all
uninitialized fields of a struct. For example, given the following struct:
uninitialized fields of an empty struct. For example, given the following struct:
type Foo struct {
ID int64
Name string
@ -54,10 +54,12 @@ func run(pass *analysis.Pass) (interface{}, error) {
return
}
expr := n.(*ast.CompositeLit)
// TODO: Handle partially-filled structs as well.
if len(expr.Elts) != 0 {
return
}
var file *ast.File
for _, f := range pass.Files {
if f.Pos() <= expr.Pos() && expr.Pos() <= f.End() {
@ -89,10 +91,19 @@ func run(pass *analysis.Pass) (interface{}, error) {
return
}
fieldCount := obj.NumFields()
if fieldCount == 0 {
// Skip any struct that is already populated or that has no fields.
if fieldCount == 0 || fieldCount == len(expr.Elts) {
return
}
var fieldSourceCode strings.Builder
// Don't mutate the existing token.File. Instead, create a copy that we can use to modify
// position information.
original := pass.Fset.File(expr.Lbrace)
fset := token.NewFileSet()
tok := fset.AddFile(original.Name(), -1, original.Size())
pos := token.Pos(1)
var elts []ast.Expr
for i := 0; i < fieldCount; i++ {
field := obj.Field(i)
// Ignore fields that are not accessible in the current package.
@ -100,46 +111,64 @@ func run(pass *analysis.Pass) (interface{}, error) {
continue
}
label := field.Name()
value := analysisinternal.ZeroValue(pass.Fset, file, pass.Pkg, field.Type())
if value == nil {
continue
}
var valBuf bytes.Buffer
if err := format.Node(&valBuf, pass.Fset, value); err != nil {
return
pos = nextLinePos(tok, pos)
kv := &ast.KeyValueExpr{
Key: &ast.Ident{
NamePos: pos,
Name: field.Name(),
},
Colon: pos,
Value: value, // 'value' has no position. fomat.Node corrects for AST nodes with no position.
}
fieldSourceCode.WriteString("\n")
fieldSourceCode.WriteString(label)
fieldSourceCode.WriteString(" : ")
fieldSourceCode.WriteString(valBuf.String())
fieldSourceCode.WriteString(",")
elts = append(elts, kv)
}
if fieldSourceCode.Len() == 0 {
// If all of the struct's fields are unexported, we have nothing to do.
if len(elts) == 0 {
return
}
fieldSourceCode.WriteString("\n")
cl := ast.CompositeLit{
Type: expr.Type, // Don't adjust the expr.Type's position.
Lbrace: token.Pos(1),
Elts: elts,
Rbrace: nextLinePos(tok, elts[len(elts)-1].Pos()),
}
buf := []byte(fieldSourceCode.String())
var buf bytes.Buffer
if err := format.Node(&buf, fset, &cl); err != nil {
return
}
msg := "Fill struct with default values"
if name, ok := expr.Type.(*ast.Ident); ok {
msg = fmt.Sprintf("Fill %s with default values", name)
}
pass.Report(analysis.Diagnostic{
Pos: expr.Lbrace,
End: expr.Rbrace,
SuggestedFixes: []analysis.SuggestedFix{{
Message: msg,
TextEdits: []analysis.TextEdit{{
Pos: expr.Lbrace + 1,
End: expr.Rbrace,
NewText: buf,
Pos: expr.Pos(),
End: expr.End(),
NewText: buf.Bytes(),
}},
}},
})
})
return nil, nil
}
func nextLinePos(tok *token.File, pos token.Pos) token.Pos {
line := tok.Line(pos)
if line+1 > tok.LineCount() {
tok.AddLine(tok.Offset(pos) + 1)
}
return tok.LineStart(line + 1)
}

View File

@ -19,11 +19,11 @@ type StructA3 struct {
func fill() {
a := StructA{
unexportedIntField : 0,
ExportedIntField : 0,
MapA : nil,
Array : nil,
StructB : StructB{},
unexportedIntField: 0,
ExportedIntField: 0,
MapA: nil,
Array: nil,
StructB: StructB{},
} //@suggestedfix("}", "refactor.rewrite")
b := StructA2{} //@suggestedfix("}", "refactor.rewrite")
c := StructA3{} //@suggestedfix("}", "refactor.rewrite")
@ -51,7 +51,7 @@ type StructA3 struct {
func fill() {
a := StructA{} //@suggestedfix("}", "refactor.rewrite")
b := StructA2{
B : nil,
B: nil,
} //@suggestedfix("}", "refactor.rewrite")
c := StructA3{} //@suggestedfix("}", "refactor.rewrite")
}
@ -79,7 +79,7 @@ func fill() {
a := StructA{} //@suggestedfix("}", "refactor.rewrite")
b := StructA2{} //@suggestedfix("}", "refactor.rewrite")
c := StructA3{
B : StructB{},
B: StructB{},
} //@suggestedfix("}", "refactor.rewrite")
}

View File

@ -12,7 +12,7 @@ type StructC struct {
func nested() {
c := StructB{
StructC: StructC{
unexportedInt : 0,
unexportedInt: 0,
}, //@suggestedfix("}", "refactor.rewrite")
}
}

View File

@ -7,7 +7,7 @@ import (
func unexported() {
a := data.A{
ExportedInt : 0,
ExportedInt: 0,
} //@suggestedfix("}", "refactor.rewrite")
}

View File

@ -7,7 +7,7 @@ type StructD struct {
func spaces() {
d := StructD{
ExportedIntField : 0,
ExportedIntField: 0,
} //@suggestedfix("}", "refactor.rewrite")
}