internal/godoc/dochtml/internal/render: fix long-literal comment rendering

The presence of a long literal caused all struct fields comments to be
removed.

This imperfectly fixes the problem by collecting all comments when
walking the declaration AST. Previously, it was thought that the
comments in an ast.CommentedNode would augment the existing comments
instead of replacing them (see golang/go#39219). The fix, as in
https://golang.org/cl/240217 and copied here, is to collect all
comments during the walk.

This isn't sufficient, because using a CommentedNode also turns off
the "contains filtered or unexported" comments that are normally added
during printing. So we have to add them back manually, which is tricky,
and can't be done perfectly (there are extra blank lines).

For golang/go#42425

Change-Id: I0bd8e5ddfc764bc3c7610575e1e3eedf9c0bfd84
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/276632
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/internal/godoc/dochtml/internal/render/linkify.go b/internal/godoc/dochtml/internal/render/linkify.go
index 3e36fac..8701aa6 100644
--- a/internal/godoc/dochtml/internal/render/linkify.go
+++ b/internal/godoc/dochtml/internal/render/linkify.go
@@ -376,7 +376,8 @@
 		return true
 	})
 
-	// Trim large string literals and slices.
+	// Trim large string literals and composite literals.
+	//ast.Fprint(os.Stdout, nil, decl, nil)
 	v := &declVisitor{}
 	ast.Walk(v, decl)
 
@@ -481,35 +482,67 @@
 
 var anchorTemplate = template.Must(template.New("anchor").Parse(`<span id="{{.ID}}" data-kind="{{.Kind}}"></span>`))
 
-// declVisitor is used to walk over the AST and trim large string
-// literals and arrays before package documentation is rendered.
-// Comments are added to Comments to indicate that a part of the
-// original code is not displayed.
+// declVisitor is an ast.Visitor that trims
+// large string literals and composite literals.
 type declVisitor struct {
+	// Comments is a collection of existing documentation in the ast.Decl,
+	// with additional comments to indicate when a part of the original
+	// code is not displayed.
 	Comments []*ast.CommentGroup
 }
 
+type afterVisitor struct {
+	v ast.Visitor
+	f func()
+}
+
+func (v *afterVisitor) Visit(n ast.Node) ast.Visitor {
+	if n == nil {
+		v.f()
+	}
+	return v.v
+}
+
 // Visit implements ast.Visitor.
 func (v *declVisitor) Visit(n ast.Node) ast.Visitor {
+
+	addComment := func(pos token.Pos, text string) {
+		v.Comments = append(v.Comments,
+			&ast.CommentGroup{List: []*ast.Comment{{
+				Slash: pos,
+				Text:  text,
+			}}})
+	}
+
 	switch n := n.(type) {
 	case *ast.BasicLit:
 		if n.Kind == token.STRING && len(n.Value) > 128 {
-			v.Comments = append(v.Comments,
-				&ast.CommentGroup{List: []*ast.Comment{{
-					Slash: n.Pos(),
-					Text:  stringBasicLitSize(n.Value),
-				}}})
+			addComment(n.Pos(), stringBasicLitSize(n.Value))
 			n.Value = `""`
 		}
 	case *ast.CompositeLit:
 		if len(n.Elts) > 100 {
-			v.Comments = append(v.Comments,
-				&ast.CommentGroup{List: []*ast.Comment{{
-					Slash: n.Lbrace,
-					Text:  fmt.Sprintf("/* %d elements not displayed */", len(n.Elts)),
-				}}})
+			addComment(n.Lbrace, fmt.Sprintf("/* %d elements not displayed */", len(n.Elts)))
 			n.Elts = n.Elts[:0]
 		}
+
+	case *ast.StructType:
+		if n.Incomplete && n.Fields != nil {
+			return &afterVisitor{v, func() {
+				addComment(n.Fields.Closing-1, "// contains filtered or unexported fields")
+			}}
+		}
+
+	case *ast.InterfaceType:
+		if n.Incomplete && n.Methods != nil {
+			return &afterVisitor{v, func() {
+				addComment(n.Methods.Closing-1, "// contains filtered or unexported methods")
+			}}
+		}
+
+	case *ast.CommentGroup:
+		v.Comments = append(v.Comments, n) // Preserve existing documentation in the ast.Decl.
+		return nil                         // No need to visit individual comments of the comment group.
 	}
 	return v
 }
diff --git a/internal/godoc/dochtml/internal/render/linkify_test.go b/internal/godoc/dochtml/internal/render/linkify_test.go
index 64ce6a1..f2f4911 100644
--- a/internal/godoc/dochtml/internal/render/linkify_test.go
+++ b/internal/godoc/dochtml/internal/render/linkify_test.go
@@ -206,6 +206,7 @@
 			want: `type Ticker struct {
 <span id="Ticker.C" data-kind="field"></span>	C &lt;-chan <a href="#Time">Time</a> <span class="comment">// The channel on which the ticks are delivered.</span>
 	<span class="comment">// contains filtered or unexported fields</span>
+
 }`,
 		},
 		{
@@ -218,12 +219,38 @@
 			symbol: "After",
 			want:   `func After(d <a href="#Duration">Duration</a>) &lt;-chan <a href="#Time">Time</a>`,
 		},
+		{
+			name:   "interface",
+			symbol: "Iface",
+			want: `type Iface interface {
+<span id="Iface.M" data-kind="method"></span>	<span class="comment">// Method comment.</span>
+	M()
+
+	<span class="comment">// contains filtered or unexported methods</span>
+
+}`,
+		},
+		{
+			name:   "long literal",
+			symbol: "TooLongLiteral",
+			want: `type TooLongLiteral struct {
+<span id="TooLongLiteral.Name" data-kind="field"></span>	<span class="comment">// The name.</span>
+	Name <a href="/builtin#string">string</a>
+
+<span id="TooLongLiteral.Labels" data-kind="field"></span>	<span class="comment">// The labels.</span>
+	Labels <a href="/builtin#int">int</a> &#34;&#34; <span class="comment">/* 137 byte string literal not displayed */</span>
+
+	<span class="comment">// contains filtered or unexported fields</span>
+
+}`,
+		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			decl := declForName(t, pkgTime, test.symbol)
 			r := New(context.Background(), fsetTime, pkgTime, nil)
 			got := r.DeclHTML("", decl).Decl.String()
 			if diff := cmp.Diff(test.want, got); diff != "" {
+				fmt.Println(got)
 				t.Errorf("mismatch (-want +got)\n%s", diff)
 			}
 		})
diff --git a/internal/godoc/dochtml/internal/render/testdata/time.go b/internal/godoc/dochtml/internal/render/testdata/time.go
index e2bf0cd..50e71eb 100644
--- a/internal/godoc/dochtml/internal/render/testdata/time.go
+++ b/internal/godoc/dochtml/internal/render/testdata/time.go
@@ -852,3 +852,20 @@
 func LoadLocation(name string) (*Location, error) {
 	return nil, nil
 }
+
+type TooLongLiteral struct {
+	// The name.
+	Name string
+
+	// The labels.
+	Labels int `A struct tag that happens to be a string literal that is too long to display, according to the stringBasicLitSize function in linkify.go.`
+
+	unexp int
+}
+
+type Iface interface {
+	// Method comment.
+	M()
+
+	u()
+}