cue/ast: allow CommentGroup as declaration

This simplifies the construction of ASTs with
free-floating comments.

Also consecutive comment groups are now always
printed with a newline in between. To not do so,
one will have to merge two consecutive comment
groups manually. This more closely matches the
typical intention of comment groups.

Change-Id: Id4a601bd6d255aa6c6489f43df52534ac5303af9
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2000
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/ast.go b/cue/ast.go
index 1db9ad8..1485a2a 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -512,6 +512,9 @@
 			}
 		}
 
+	case *ast.CommentGroup:
+		// Nothing to do for a free-floating comment group.
+
 	// nothing to do
 	// case *syntax.EmitDecl:
 	default:
diff --git a/cue/ast/ast.go b/cue/ast/ast.go
index cc2e7eb..bd71220 100644
--- a/cue/ast/ast.go
+++ b/cue/ast/ast.go
@@ -85,6 +85,7 @@
 func (*BadDecl) declNode()           {}
 func (*EmitDecl) declNode()          {}
 func (*Alias) declNode()             {}
+func (*CommentGroup) declNode()      {}
 
 // A Label is any prduction that can be used as a LHS label.
 type Label interface {
@@ -177,7 +178,7 @@
 // A CommentGroup represents a sequence of comments
 // with no other tokens and no empty lines between.
 type CommentGroup struct {
-	// TODO: remove and use the token position of the first commment.
+	// TODO: remove and use the token position of the first comment.
 	Doc  bool
 	Line bool // true if it is on the same line as the node's end pos.
 
@@ -630,6 +631,9 @@
 	if d.Rparen.IsValid() {
 		return d.Rparen.Add(1)
 	}
+	if len(d.Specs) == 0 {
+		return token.NoPos
+	}
 	return d.Specs[0].End()
 }
 func (d *EmitDecl) End() token.Pos { return d.Expr.End() }
@@ -647,9 +651,10 @@
 	comments
 	Package token.Pos // position of "package" pseudo-keyword
 	Name    *Ident    // package names
+	Decls   []Decl    // top-level declarations; or nil
+
 	// TODO: Change Expr to Decl?
 	Imports    []*ImportSpec // imports in this file
-	Decls      []Decl        // top-level declarations; or nil
 	Unresolved []*Ident      // unresolved identifiers in this file
 }
 
@@ -662,6 +667,7 @@
 	}
 	return token.NoPos
 }
+
 func (f *File) End() token.Pos {
 	if n := len(f.Decls); n > 0 {
 		return f.Decls[n-1].End()
diff --git a/cue/ast/walk.go b/cue/ast/walk.go
index ce697ce..f11d06f 100644
--- a/cue/ast/walk.go
+++ b/cue/ast/walk.go
@@ -100,9 +100,7 @@
 		}
 
 	case *StructLit:
-		for _, f := range n.Elts {
-			walk(v, f)
-		}
+		walkDeclList(v, n.Elts)
 
 	// Expressions
 	case *BottomLit, *BadExpr, *Ident, *BasicLit:
@@ -187,9 +185,6 @@
 			walk(v, n.Name)
 		}
 		walkDeclList(v, n.Decls)
-		// don't walk n.Comments - they have been
-		// visited already through the individual
-		// nodes
 
 	case *ListComprehension:
 		walk(v, n.Expr)
diff --git a/cue/format/format.go b/cue/format/format.go
index 30df812..5b72b23 100644
--- a/cue/format/format.go
+++ b/cue/format/format.go
@@ -299,28 +299,37 @@
 func (f *formatter) visitComments(until int8) {
 	c := &f.current
 
+	printed := false
 	for ; len(c.cg) > 0 && c.cg[0].Position <= until; c.cg = c.cg[1:] {
-		f.Print(c.cg[0])
-
-		printBlank := false
-		if c.cg[0].Doc {
-			f.Print(newline)
-			printBlank = true
+		if printed {
+			f.Print(newsection)
 		}
-		for _, c := range c.cg[0].List {
-			isEnd := strings.HasPrefix(c.Text, "//")
-			if !printBlank {
-				if isEnd {
-					f.Print(vtab)
-				} else {
-					f.Print(blank)
-				}
-			}
-			f.Print(c.Slash)
-			f.Print(c)
+		printed = true
+		f.printComment(c.cg[0])
+	}
+}
+
+func (f *formatter) printComment(cg *ast.CommentGroup) {
+	f.Print(cg)
+
+	printBlank := false
+	if cg.Doc {
+		f.Print(newline)
+		printBlank = true
+	}
+	for _, c := range cg.List {
+		isEnd := strings.HasPrefix(c.Text, "//")
+		if !printBlank {
 			if isEnd {
-				f.Print(newline)
+				f.Print(vtab)
+			} else {
+				f.Print(blank)
 			}
 		}
+		f.Print(c.Slash)
+		f.Print(c)
+		if isEnd {
+			f.Print(newline)
+		}
 	}
 }
diff --git a/cue/format/node.go b/cue/format/node.go
index a683890..527c92e 100644
--- a/cue/format/node.go
+++ b/cue/format/node.go
@@ -207,6 +207,7 @@
 		f.print(n.Import, "import")
 		if len(n.Specs) == 0 {
 			f.print(blank, n.Lparen, token.LPAREN, n.Rparen, token.RPAREN, newline)
+			break
 		}
 		switch {
 		case len(n.Specs) == 1:
@@ -232,6 +233,11 @@
 		f.print(blank, n.Equal, token.BIND, blank)
 		f.expr(n.Expr)
 		f.print(declcomma, newline) // implied
+
+	case *ast.CommentGroup:
+		f.print(newsection)
+		f.printComment(n)
+		f.print(newsection)
 	}
 after:
 	f.after(decl)
diff --git a/cue/parser/walk.go b/cue/parser/walk.go
index 01a8220..f952f50 100644
--- a/cue/parser/walk.go
+++ b/cue/parser/walk.go
@@ -181,9 +181,6 @@
 			walk(v, n.Name)
 		}
 		walkDeclList(v, n.Decls)
-		// don't walk n.Comments - they have been
-		// visited already through the individual
-		// nodes
 
 	case *ast.ListComprehension:
 		walk(v, n.Expr)