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>
diff --git a/internal/lsp/analysis/fillstruct/fillstruct.go b/internal/lsp/analysis/fillstruct/fillstruct.go
index 6ec8a35..23e1662 100644
--- a/internal/lsp/analysis/fillstruct/fillstruct.go
+++ b/internal/lsp/analysis/fillstruct/fillstruct.go
@@ -11,8 +11,8 @@
 	"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 @@
 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 @@
 			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 @@
 			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 @@
 				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)
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go.golden b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go.golden
index 13c0a0e..162b4c4 100644
--- a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go.golden
@@ -19,11 +19,11 @@
 
 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 @@
 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 @@
 	a := StructA{}  //@suggestedfix("}", "refactor.rewrite")
 	b := StructA2{} //@suggestedfix("}", "refactor.rewrite")
 	c := StructA3{
-B : StructB{},
+	B: StructB{},
 } //@suggestedfix("}", "refactor.rewrite")
 }
 
diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go.golden b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go.golden
index 43a7b5b..688ba77 100644
--- a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_nested.go.golden
@@ -12,7 +12,7 @@
 func nested() {
 	c := StructB{
 		StructC: StructC{
-unexportedInt : 0,
+	unexportedInt: 0,
 }, //@suggestedfix("}", "refactor.rewrite")
 	}
 }
diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go.golden b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go.golden
index f11a5af..d15f5a1 100644
--- a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_package.go.golden
@@ -7,7 +7,7 @@
 
 func unexported() {
 	a := data.A{
-ExportedInt : 0,
+	ExportedInt: 0,
 } //@suggestedfix("}", "refactor.rewrite")
 }
 
diff --git a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_spaces.go.golden b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_spaces.go.golden
index 33643a6..b0e8937 100644
--- a/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_spaces.go.golden
+++ b/internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct_spaces.go.golden
@@ -7,7 +7,7 @@
 
 func spaces() {
 	d := StructD{
-ExportedIntField : 0,
+	ExportedIntField: 0,
 } //@suggestedfix("}", "refactor.rewrite")
 }