Skip to content

Commit 8171d94

Browse files
committed
gopls/internal/analysis/fillstruct: preserve existing formatting
Modifies fillstruct refactoring to preserve the formatting and order of prefilled struct elements and comments. Fixes golang/go#70690, golang/go#71312 Change-Id: I0879d22a392e6c3ab85621420e54eb2e4651a1db Reviewed-on: https://go-review.googlesource.com/c/tools/+/643696 Reviewed-by: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent ac81e9f commit 8171d94

File tree

3 files changed

+124
-88
lines changed

3 files changed

+124
-88
lines changed

gopls/internal/analysis/fillstruct/fillstruct.go

Lines changed: 76 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"fmt"
1818
"go/ast"
1919
"go/format"
20+
"go/printer"
2021
"go/token"
2122
"go/types"
2223
"strings"
@@ -168,26 +169,16 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil
168169
// Check which types have already been filled in. (we only want to fill in
169170
// the unfilled types, or else we'll blat user-supplied details)
170171
prefilledFields := map[string]ast.Expr{}
172+
var elts []ast.Expr
171173
for _, e := range expr.Elts {
172174
if kv, ok := e.(*ast.KeyValueExpr); ok {
173175
if key, ok := kv.Key.(*ast.Ident); ok {
174176
prefilledFields[key.Name] = kv.Value
177+
elts = append(elts, kv)
175178
}
176179
}
177180
}
178181

179-
// Use a new fileset to build up a token.File for the new composite
180-
// literal. We need one line for foo{, one line for }, and one line for
181-
// each field we're going to set. format.Node only cares about line
182-
// numbers, so we don't need to set columns, and each line can be
183-
// 1 byte long.
184-
// TODO(adonovan): why is this necessary? The position information
185-
// is going to be wrong for the existing trees in prefilledFields.
186-
// Can't the formatter just do its best with an empty fileset?
187-
fakeFset := token.NewFileSet()
188-
tok := fakeFset.AddFile("", -1, fieldCount+2)
189-
190-
line := 2 // account for 1-based lines and the left brace
191182
var fieldTyps []types.Type
192183
for i := 0; i < fieldCount; i++ {
193184
field := tStruct.Field(i)
@@ -200,69 +191,48 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil
200191
}
201192
matches := analysisinternal.MatchingIdents(fieldTyps, file, start, info, pkg)
202193
qual := typesinternal.FileQualifier(file, pkg)
203-
var elts []ast.Expr
194+
204195
for i, fieldTyp := range fieldTyps {
205196
if fieldTyp == nil {
206197
continue // TODO(adonovan): is this reachable?
207198
}
208199
fieldName := tStruct.Field(i).Name()
209-
210-
tok.AddLine(line - 1) // add 1 byte per line
211-
if line > tok.LineCount() {
212-
panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct", line, tok.LineCount()))
200+
if _, ok := prefilledFields[fieldName]; ok {
201+
// We already stored these when looping over expr.Elt.
202+
// Want to preserve the original order of prefilled fields
203+
continue
213204
}
214-
pos := tok.LineStart(line)
215205

216206
kv := &ast.KeyValueExpr{
217207
Key: &ast.Ident{
218-
NamePos: pos,
219-
Name: fieldName,
208+
Name: fieldName,
220209
},
221-
Colon: pos,
222210
}
223-
if expr, ok := prefilledFields[fieldName]; ok {
211+
212+
names, ok := matches[fieldTyp]
213+
if !ok {
214+
return nil, nil, fmt.Errorf("invalid struct field type: %v", fieldTyp)
215+
}
216+
217+
// Find the name most similar to the field name.
218+
// If no name matches the pattern, generate a zero value.
219+
// NOTE: We currently match on the name of the field key rather than the field type.
220+
if best := fuzzy.BestMatch(fieldName, names); best != "" {
221+
kv.Value = ast.NewIdent(best)
222+
} else if expr, isValid := populateValue(fieldTyp, qual); isValid {
224223
kv.Value = expr
225224
} else {
226-
names, ok := matches[fieldTyp]
227-
if !ok {
228-
return nil, nil, fmt.Errorf("invalid struct field type: %v", fieldTyp)
229-
}
230-
231-
// Find the name most similar to the field name.
232-
// If no name matches the pattern, generate a zero value.
233-
// NOTE: We currently match on the name of the field key rather than the field type.
234-
if best := fuzzy.BestMatch(fieldName, names); best != "" {
235-
kv.Value = ast.NewIdent(best)
236-
} else if expr, isValid := populateValue(fieldTyp, qual); isValid {
237-
kv.Value = expr
238-
} else {
239-
return nil, nil, nil // no fix to suggest
240-
}
225+
return nil, nil, nil // no fix to suggest
241226
}
227+
242228
elts = append(elts, kv)
243-
line++
244229
}
245230

246231
// If all of the struct's fields are unexported, we have nothing to do.
247232
if len(elts) == 0 {
248233
return nil, nil, fmt.Errorf("no elements to fill")
249234
}
250235

251-
// Add the final line for the right brace. Offset is the number of
252-
// bytes already added plus 1.
253-
tok.AddLine(len(elts) + 1)
254-
line = len(elts) + 2
255-
if line > tok.LineCount() {
256-
panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct", line, tok.LineCount()))
257-
}
258-
259-
cl := &ast.CompositeLit{
260-
Type: expr.Type,
261-
Lbrace: tok.LineStart(1),
262-
Elts: elts,
263-
Rbrace: tok.LineStart(line),
264-
}
265-
266236
// Find the line on which the composite literal is declared.
267237
split := bytes.Split(content, []byte("\n"))
268238
lineNumber := safetoken.StartPosition(fset, expr.Lbrace).Line
@@ -274,26 +244,66 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil
274244
index := bytes.Index(firstLine, trimmed)
275245
whitespace := firstLine[:index]
276246

277-
// First pass through the formatter: turn the expr into a string.
278-
var formatBuf bytes.Buffer
279-
if err := format.Node(&formatBuf, fakeFset, cl); err != nil {
280-
return nil, nil, fmt.Errorf("failed to run first format on:\n%s\ngot err: %v", cl.Type, err)
281-
}
282-
sug := indent(formatBuf.Bytes(), whitespace)
247+
// Write a new composite literal "_{...}" composed of all prefilled and new elements,
248+
// preserving existing formatting and comments.
249+
// An alternative would be to only format the new fields,
250+
// but by printing the entire composite literal, we ensure
251+
// that the result is gofmt'ed.
252+
var buf bytes.Buffer
253+
buf.WriteString("_{\n")
254+
fcmap := ast.NewCommentMap(fset, file, file.Comments)
255+
comments := fcmap.Filter(expr).Comments() // comments inside the expr, in source order
256+
for _, elt := range elts {
257+
// Print comments before the current elt
258+
for len(comments) > 0 && comments[0].Pos() < elt.Pos() {
259+
for _, co := range comments[0].List {
260+
fmt.Fprintln(&buf, co.Text)
261+
}
262+
comments = comments[1:]
263+
}
264+
265+
// Print the current elt with comments
266+
eltcomments := fcmap.Filter(elt).Comments()
267+
if err := format.Node(&buf, fset, &printer.CommentedNode{Node: elt, Comments: eltcomments}); err != nil {
268+
return nil, nil, err
269+
}
270+
buf.WriteString(",")
283271

284-
if len(prefilledFields) > 0 {
285-
// Attempt a second pass through the formatter to line up columns.
286-
sourced, err := format.Source(sug)
287-
if err == nil {
288-
sug = indent(sourced, whitespace)
272+
// Prune comments up to the end of the elt
273+
for len(comments) > 0 && comments[0].Pos() < elt.End() {
274+
comments = comments[1:]
289275
}
276+
277+
// Write comments associated with the current elt that appear after it
278+
// printer.CommentedNode only prints comments inside the elt.
279+
for _, cg := range eltcomments {
280+
for _, co := range cg.List {
281+
if co.Pos() >= elt.End() {
282+
fmt.Fprintln(&buf, co.Text)
283+
if len(comments) > 0 {
284+
comments = comments[1:]
285+
}
286+
}
287+
}
288+
}
289+
buf.WriteString("\n")
290+
}
291+
buf.WriteString("}")
292+
formatted, err := format.Source(buf.Bytes())
293+
if err != nil {
294+
return nil, nil, err
290295
}
291296

297+
sug := indent(formatted, whitespace)
298+
// Remove _
299+
idx := bytes.IndexByte(sug, '{') // cannot fail
300+
sug = sug[idx:]
301+
292302
return fset, &analysis.SuggestedFix{
293303
TextEdits: []analysis.TextEdit{
294304
{
295-
Pos: expr.Pos(),
296-
End: expr.End(),
305+
Pos: expr.Lbrace,
306+
End: expr.Rbrace + token.Pos(len("}")),
297307
NewText: sug,
298308
},
299309
},

gopls/internal/test/marker/testdata/codeaction/fill_struct.txt

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -364,12 +364,15 @@ func fill() {
364364
_ := StructAnon{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon)
365365
}
366366
-- @fillStruct_anon/fillStruct_anon.go --
367-
@@ -13 +13,5 @@
367+
@@ -13 +13,8 @@
368368
- _ := StructAnon{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon)
369369
+ _ := StructAnon{
370370
+ a: struct{}{},
371371
+ b: map[string]any{},
372-
+ c: map[string]struct{d int; e bool}{},
372+
+ c: map[string]struct {
373+
+ d int
374+
+ e bool
375+
+ }{},
373376
+ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon)
374377
-- fillStruct_nested.go --
375378
package fillstruct
@@ -457,13 +460,8 @@ func fill() {
457460
+ UnfilledInt: 0,
458461
+ StructPartialB: StructPartialB{},
459462
-- @fillStruct_partial2/fillStruct_partial.go --
460-
@@ -19,4 +19,2 @@
461-
- /* this comment should disappear */
462-
- PrefilledInt: 7, // This comment should be blown away.
463-
- /* As should
464-
- this one */
465-
+ PrefilledInt: 7,
466-
+ UnfilledInt: 0,
463+
@@ -23 +23 @@
464+
+ UnfilledInt: 0,
467465
-- fillStruct_spaces.go --
468466
package fillstruct
469467

@@ -566,7 +564,7 @@ func _[T any]() {
566564
+ bar: 0,
567565
+} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams2)
568566
-- @typeparams3/typeparams.go --
569-
@@ -21 +21 @@
567+
@@ -22 +22 @@
570568
+ foo: 0,
571569
-- @typeparams4/typeparams.go --
572570
@@ -29 +29,4 @@
@@ -723,3 +721,33 @@ func _() {
723721
+ aliasArray: aliasArray{},
724722
+ aliasNamed: aliasNamed{},
725723
+ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=alias)
724+
-- preserveformat/preserveformat.go --
725+
package preserveformat
726+
727+
type (
728+
Node struct {
729+
Value int
730+
}
731+
Graph struct {
732+
Nodes []*Node `json:""`
733+
Edges map[*Node]*Node
734+
Other string
735+
}
736+
)
737+
738+
func _() {
739+
_ := &Graph{
740+
// comments at the start preserved
741+
Nodes: []*Node{
742+
{Value: 0}, // comments in the middle preserved
743+
// between lines
744+
{Value: 0},
745+
}, // another comment
746+
// comment group
747+
// below
748+
} //@codeaction("}", "refactor.rewrite.fillStruct", edit=preserveformat)
749+
}
750+
-- @preserveformat/preserveformat/preserveformat.go --
751+
@@ -24 +24,2 @@
752+
+ Edges: map[*Node]*Node{},
753+
+ Other: "",

gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -373,12 +373,15 @@ func fill() {
373373
_ := StructAnon{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon)
374374
}
375375
-- @fillStruct_anon/fillStruct_anon.go --
376-
@@ -13 +13,5 @@
376+
@@ -13 +13,8 @@
377377
- _ := StructAnon{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon)
378378
+ _ := StructAnon{
379379
+ a: struct{}{},
380380
+ b: map[string]any{},
381-
+ c: map[string]struct{d int; e bool}{},
381+
+ c: map[string]struct {
382+
+ d int
383+
+ e bool
384+
+ }{},
382385
+ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_anon)
383386
-- fillStruct_nested.go --
384387
package fillstruct
@@ -452,8 +455,8 @@ func fill() {
452455
PrefilledInt: 5,
453456
} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_partial1)
454457
b := StructPartialB{
455-
/* this comment should disappear */
456-
PrefilledInt: 7, // This comment should be blown away.
458+
/* this comment should be preserved */
459+
PrefilledInt: 7, // This comment should be preserved.
457460
/* As should
458461
this one */
459462
} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fillStruct_partial2)
@@ -466,13 +469,8 @@ func fill() {
466469
+ UnfilledInt: 0,
467470
+ StructPartialB: StructPartialB{},
468471
-- @fillStruct_partial2/fillStruct_partial.go --
469-
@@ -19,4 +19,2 @@
470-
- /* this comment should disappear */
471-
- PrefilledInt: 7, // This comment should be blown away.
472-
- /* As should
473-
- this one */
474-
+ PrefilledInt: 7,
475-
+ UnfilledInt: 0,
472+
@@ -23 +23 @@
473+
+ UnfilledInt: 0,
476474
-- fillStruct_spaces.go --
477475
package fillstruct
478476

@@ -575,7 +573,7 @@ func _[T any]() {
575573
+ bar: 0,
576574
+} //@codeaction("}", "refactor.rewrite.fillStruct", edit=typeparams2)
577575
-- @typeparams3/typeparams.go --
578-
@@ -21 +21 @@
576+
@@ -22 +22 @@
579577
+ foo: 0,
580578
-- @typeparams4/typeparams.go --
581579
@@ -29 +29,4 @@

0 commit comments

Comments
 (0)