Skip to content

Commit 59ef3a9

Browse files
committed
go/doc: group play example imports
When synthesizing a program from a playable example, preserve the grouping of imports. That is, maintain blank lines between imports while removing unused ones. People are used to having those groups because that is what goimports does. It's disconcerting to see the all imports placed together, as the existing code does, especially when the user has already grouped them. For an example, see #43658. This is an improvement to a fix in pkgsite's fork of go/doc (https://go.googlesource.com/pkgsite/+/7b10ef3861af4a863bf215f63b6de94c681d5af0/internal/godoc/internal/doc/example_pkgsite.go#405). Here I've managed to avoid using a token.FileSet. Change-Id: I65605e6dd53d742a3fe1210c3f982b54e3706198 Reviewed-on: https://go-review.googlesource.com/c/go/+/384837 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Robert Findley <[email protected]> Run-TryBot: Jonathan Amsterdam <[email protected]>
1 parent 1ce7fcf commit 59ef3a9

File tree

3 files changed

+180
-12
lines changed

3 files changed

+180
-12
lines changed

src/go/doc/example.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,22 @@ func playExample(file *ast.File, f *ast.FuncDecl) *ast.File {
270270
// Use unresolved identifiers to determine the imports used by this
271271
// example. The heuristic assumes package names match base import
272272
// paths for imports w/o renames (should be good enough most of the time).
273-
namedImports := make(map[string]string) // [name]path
274-
var blankImports []ast.Spec // _ imports
273+
var namedImports []ast.Spec
274+
var blankImports []ast.Spec // _ imports
275+
276+
// To preserve the blank lines between groups of imports, find the
277+
// start position of each group, and assign that position to all
278+
// imports from that group.
279+
groupStarts := findImportGroupStarts(file.Imports)
280+
groupStart := func(s *ast.ImportSpec) token.Pos {
281+
for i, start := range groupStarts {
282+
if s.Path.ValuePos < start {
283+
return groupStarts[i-1]
284+
}
285+
}
286+
return groupStarts[len(groupStarts)-1]
287+
}
288+
275289
for _, s := range file.Imports {
276290
p, err := strconv.Unquote(s.Path.Value)
277291
if err != nil {
@@ -295,7 +309,12 @@ func playExample(file *ast.File, f *ast.FuncDecl) *ast.File {
295309
}
296310
}
297311
if unresolved[n] {
298-
namedImports[n] = p
312+
// Copy the spec and its path to avoid modifying the original.
313+
spec := *s
314+
path := *s.Path
315+
spec.Path = &path
316+
spec.Path.ValuePos = groupStart(&spec)
317+
namedImports = append(namedImports, &spec)
299318
delete(unresolved, n)
300319
}
301320
}
@@ -345,14 +364,7 @@ func playExample(file *ast.File, f *ast.FuncDecl) *ast.File {
345364
Lparen: 1, // Need non-zero Lparen and Rparen so that printer
346365
Rparen: 1, // treats this as a factored import.
347366
}
348-
for n, p := range namedImports {
349-
s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
350-
if path.Base(p) != n {
351-
s.Name = ast.NewIdent(n)
352-
}
353-
importDecl.Specs = append(importDecl.Specs, s)
354-
}
355-
importDecl.Specs = append(importDecl.Specs, blankImports...)
367+
importDecl.Specs = append(namedImports, blankImports...)
356368

357369
// Synthesize main function.
358370
funcDecl := &ast.FuncDecl{
@@ -369,7 +381,6 @@ func playExample(file *ast.File, f *ast.FuncDecl) *ast.File {
369381
sort.Slice(decls, func(i, j int) bool {
370382
return decls[i].Pos() < decls[j].Pos()
371383
})
372-
373384
sort.Slice(comments, func(i, j int) bool {
374385
return comments[i].Pos() < comments[j].Pos()
375386
})
@@ -382,6 +393,41 @@ func playExample(file *ast.File, f *ast.FuncDecl) *ast.File {
382393
}
383394
}
384395

396+
// findImportGroupStarts finds the start positions of each sequence of import
397+
// specs that are not separated by a blank line.
398+
func findImportGroupStarts(imps []*ast.ImportSpec) []token.Pos {
399+
startImps := findImportGroupStarts1(imps)
400+
groupStarts := make([]token.Pos, len(startImps))
401+
for i, imp := range startImps {
402+
groupStarts[i] = imp.Pos()
403+
}
404+
return groupStarts
405+
}
406+
407+
// Helper for findImportGroupStarts to ease testing.
408+
func findImportGroupStarts1(origImps []*ast.ImportSpec) []*ast.ImportSpec {
409+
// Copy to avoid mutation.
410+
imps := make([]*ast.ImportSpec, len(origImps))
411+
copy(imps, origImps)
412+
// Assume the imports are sorted by position.
413+
sort.Slice(imps, func(i, j int) bool { return imps[i].Pos() < imps[j].Pos() })
414+
// Assume gofmt has been applied, so there is a blank line between adjacent imps
415+
// if and only if they are more than 2 positions apart (newline, tab).
416+
var groupStarts []*ast.ImportSpec
417+
prevEnd := token.Pos(-2)
418+
for _, imp := range imps {
419+
if imp.Pos()-prevEnd > 2 {
420+
groupStarts = append(groupStarts, imp)
421+
}
422+
prevEnd = imp.End()
423+
// Account for end-of-line comments.
424+
if imp.Comment != nil {
425+
prevEnd = imp.Comment.End()
426+
}
427+
}
428+
return groupStarts
429+
}
430+
385431
// playExampleFile takes a whole file example and synthesizes a new *ast.File
386432
// such that the example is function main in package main.
387433
func playExampleFile(file *ast.File) *ast.File {

src/go/doc/example_internal_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package doc
6+
7+
import (
8+
"go/parser"
9+
"go/token"
10+
"reflect"
11+
"strconv"
12+
"strings"
13+
"testing"
14+
)
15+
16+
func TestImportGroupStarts(t *testing.T) {
17+
for _, test := range []struct {
18+
name string
19+
in string
20+
want []string // paths of group-starting imports
21+
}{
22+
{
23+
name: "one group",
24+
in: `package p
25+
import (
26+
"a"
27+
"b"
28+
"c"
29+
"d"
30+
)
31+
`,
32+
want: []string{"a"},
33+
},
34+
{
35+
name: "several groups",
36+
in: `package p
37+
import (
38+
"a"
39+
40+
"b"
41+
"c"
42+
43+
"d"
44+
)
45+
`,
46+
want: []string{"a", "b", "d"},
47+
},
48+
{
49+
name: "extra space",
50+
in: `package p
51+
import (
52+
"a"
53+
54+
55+
"b"
56+
"c"
57+
58+
59+
"d"
60+
)
61+
`,
62+
want: []string{"a", "b", "d"},
63+
},
64+
{
65+
name: "line comment",
66+
in: `package p
67+
import (
68+
"a" // comment
69+
"b" // comment
70+
71+
"c"
72+
)`,
73+
want: []string{"a", "c"},
74+
},
75+
{
76+
name: "named import",
77+
in: `package p
78+
import (
79+
"a"
80+
n "b"
81+
82+
m "c"
83+
"d"
84+
)`,
85+
want: []string{"a", "c"},
86+
},
87+
{
88+
name: "blank import",
89+
in: `package p
90+
import (
91+
"a"
92+
93+
_ "b"
94+
95+
_ "c"
96+
"d"
97+
)`,
98+
want: []string{"a", "b", "c"},
99+
},
100+
} {
101+
t.Run(test.name, func(t *testing.T) {
102+
fset := token.NewFileSet()
103+
file, err := parser.ParseFile(fset, "test.go", strings.NewReader(test.in), parser.ParseComments)
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
imps := findImportGroupStarts1(file.Imports)
108+
got := make([]string, len(imps))
109+
for i, imp := range imps {
110+
got[i], err = strconv.Unquote(imp.Path.Value)
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
}
115+
if !reflect.DeepEqual(got, test.want) {
116+
t.Errorf("got %v, want %v", got, test.want)
117+
}
118+
})
119+
}
120+
121+
}

src/go/doc/example_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ func TestExampleEmpty(t *testing.T) {
522522
}
523523

524524
func formatFile(t *testing.T, fset *token.FileSet, n *ast.File) string {
525+
t.Helper()
525526
if n == nil {
526527
return "<nil>"
527528
}

0 commit comments

Comments
 (0)