cmd/cue/cmd: move get to to use the ast

Using the AST makes it easier to to resolve
Issue #228.

Change-Id: Idec116806aa80fc1ea283911759a391059682571
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4384
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/get_go.go b/cmd/cue/cmd/get_go.go
index 9945f90..b3de7a1 100644
--- a/cmd/cue/cmd/get_go.go
+++ b/cmd/cue/cmd/get_go.go
@@ -251,13 +251,11 @@
 	usedPkgs map[string]bool
 
 	// per file
-	w          *bytes.Buffer
 	cmap       ast.CommentMap
 	pkg        *packages.Package
 	consts     map[string][]string
 	pkgNames   map[string]pkgInfo
 	usedInFile map[string]bool
-	indent     int
 
 	exclusions []*regexp.Regexp
 	exclude    string
@@ -408,8 +406,6 @@
 	}
 
 	for i, f := range p.Syntax {
-		e.w = &bytes.Buffer{}
-
 		e.cmap = ast.NewCommentMap(p.Fset, f, f.Comments)
 
 		e.pkgNames = map[string]pkgInfo{}
@@ -431,17 +427,15 @@
 			e.pkgNames[pkgPath] = info
 		}
 
-		hasEntries := false
+		decls := []cueast.Decl{}
 		for _, d := range f.Decls {
 			switch x := d.(type) {
 			case *ast.GenDecl:
-				if e.reportDecl(e.w, x) {
-					hasEntries = true
-				}
+				decls = append(decls, e.reportDecl(x)...)
 			}
 		}
 
-		if !hasEntries && f.Doc == nil {
+		if len(decls) == 0 && f.Doc == nil {
 			continue
 		}
 
@@ -451,43 +445,38 @@
 		}
 		sort.Strings(pkgs)
 
-		w := &bytes.Buffer{}
+		pkg := &cueast.Package{Name: e.ident(p.Name)}
+		addDoc(f.Doc, pkg)
 
-		fmt.Fprintln(w, "// Code generated by cue get go. DO NOT EDIT.")
-		fmt.Fprintln(w)
-		fmt.Fprintln(w, "//cue:generate cue get go", args)
-		fmt.Fprintln(w)
-		if f.Doc != nil {
-			writeDoc(w, f.Doc)
-		}
-		fmt.Fprintf(w, "package %s\n", p.Name)
-		fmt.Fprintln(w)
+		f := &cueast.File{Decls: []cueast.Decl{
+			internal.NewComment(false, "Code generated by cue get go. DO NOT EDIT."),
+			&cueast.CommentGroup{List: []*cueast.Comment{
+				{Text: "//cue:generate cue get go " + args},
+			}},
+			pkg,
+		}}
+
 		if len(pkgs) > 0 {
-			fmt.Fprintln(w, "import (")
+			imports := &cueast.ImportDecl{}
+			f.Decls = append(f.Decls, imports)
 			for _, s := range pkgs {
 				info := e.pkgNames[s]
-				if p.Imports[s].Name == info.name {
-					fmt.Fprintf(w, "%q\n", info.id)
-				} else {
-					fmt.Fprintf(w, "%v %q\n", info.name, info.id)
+				spec := cueast.NewImport(nil, info.id)
+				if p.Imports[s].Name != info.name {
+					spec.Name = e.ident(info.name)
 				}
+				imports.Specs = append(imports.Specs, spec)
 			}
-			fmt.Fprintln(w, ")")
-			fmt.Fprintln(w)
 		}
-		fmt.Fprintln(w)
-		_, _ = io.Copy(w, e.w)
+
+		f.Decls = append(f.Decls, decls...)
 
 		file := filepath.Base(p.CompiledGoFiles[i])
 
 		file = strings.Replace(file, ".go", "_go", 1)
 		file += "_gen.cue"
-		b, err := format.Source(w.Bytes())
+		b, err := format.Node(f)
 		if err != nil {
-			_ = ioutil.WriteFile(filepath.Join(dir, file), w.Bytes(), 0644)
-			stderr := e.cmd.Stderr()
-			fmt.Fprintln(stderr, w.String())
-			fmt.Fprintln(stderr, dir, file)
 			return err
 		}
 		err = ioutil.WriteFile(filepath.Join(dir, file), b, 0644)
@@ -568,7 +557,31 @@
 	}
 }
 
-func (e *extractor) reportDecl(w io.Writer, x *ast.GenDecl) (added bool) {
+func (e *extractor) label(name string) cueast.Label {
+	if !cueast.IsValidIdent(name) {
+		return cueast.NewString(name)
+	}
+	return cueast.NewIdent(name)
+}
+
+func (e *extractor) ident(name string) *cueast.Ident {
+	return cueast.NewIdent(name)
+}
+
+func (e *extractor) def(doc *ast.CommentGroup, name string, value cueast.Expr, newline bool) *cueast.Field {
+	f := &cueast.Field{
+		Label: e.label(name),
+		Token: cuetoken.ISA,
+		Value: value,
+	}
+	addDoc(doc, f)
+	if newline {
+		cueast.SetRelPos(f, cuetoken.NewSection)
+	}
+	return f
+}
+
+func (e *extractor) reportDecl(x *ast.GenDecl) (a []cueast.Decl) {
 	switch x.Tok {
 	case token.TYPE:
 		for _, s := range x.Specs {
@@ -582,12 +595,10 @@
 			name := v.Name.Name
 			switch tn, ok := e.pkg.TypesInfo.Defs[v.Name].(*types.TypeName); {
 			case ok:
-				if altType := e.altType(tn.Type()); altType != "" {
+				if altType := e.altType(tn.Type()); altType != nil {
 					// TODO: add the underlying tag as a Go tag once we have
 					// proper string escaping for CUE.
-					e.printDoc(x.Doc, true)
-					fmt.Fprintf(e.w, "%s :: %s", name, altType)
-					added = true
+					a = append(a, e.def(x.Doc, name, altType, true))
 					break
 				}
 				fallthrough
@@ -597,40 +608,37 @@
 					e.logf("    Dropped declaration %v of unsupported type %v", name, typ)
 					continue
 				}
-				added = true
-
-				if s := e.altType(types.NewPointer(typ)); s != "" {
-					e.printDoc(x.Doc, true)
-					fmt.Fprint(e.w, name, " :: ", s)
+				if s := e.altType(types.NewPointer(typ)); s != nil {
+					a = append(a, e.def(x.Doc, name, s, true))
 					break
 				}
 				// TODO: only print original type if value is not marked as enum.
 				underlying := e.pkg.TypesInfo.TypeOf(v.Type)
-				e.printField(name, cuetoken.ISA, underlying, x.Doc, true)
+				f, _ := e.makeField(name, cuetoken.ISA, underlying, x.Doc, true)
+				a = append(a, f)
+				cueast.SetRelPos(f, cuetoken.NewSection)
+
 			}
 
-			e.indent++
 			if len(enums) > 0 {
-				fmt.Fprintf(e.w, " // enum%s", name)
+				enumName := "enum" + name
+				a[len(a)-1].AddComment(internal.NewComment(false, enumName))
 
-				e.newLine()
-				e.newLine()
-				fmt.Fprintf(e.w, "enum%s::\n%v", name, enums[0])
+				var x cueast.Expr = e.ident(enums[0])
+				cueast.SetRelPos(x, cuetoken.Newline)
 				for _, v := range enums[1:] {
-					fmt.Fprint(e.w, " |")
-					e.newLine()
-					fmt.Fprint(e.w, v)
+					y := e.ident(v)
+					cueast.SetRelPos(y, cuetoken.Newline)
+					x = &cueast.BinaryExpr{X: x, Op: cuetoken.OR, Y: y}
 				}
+				a = append(a, e.def(nil, enumName, x, true))
 			}
-			e.indent--
-			e.newLine()
-			e.newLine()
 		}
 
 	case token.CONST:
 		// TODO: copy over comments for constant blocks.
 
-		for _, s := range x.Specs {
+		for k, s := range x.Specs {
 			// TODO: determine type name and filter.
 			v, ok := s.(*ast.ValueSpec)
 			if !ok {
@@ -641,58 +649,50 @@
 				if !ast.IsExported(name.Name) {
 					continue
 				}
-				added = true
-
-				e.printDoc(v.Doc, true)
-				fmt.Fprint(e.w, name.Name, " :: ")
-
-				typ := e.pkg.TypesInfo.TypeOf(name)
-				if s := typ.String(); !strings.Contains(s, "untyped") {
-					switch s {
-					case "byte", "string", "error":
-					default:
-						e.printType(typ)
-						fmt.Fprint(e.w, " & ")
-					}
-				}
+				f := e.def(v.Doc, name.Name, nil, k == 0)
+				a = append(a, f)
 
 				val := ""
-				comment := ""
 				if i < len(v.Values) {
 					if lit, ok := v.Values[i].(*ast.BasicLit); ok {
 						val = lit.Value
 					}
 				}
 
-			outer:
-				switch {
-				case len(val) <= 1:
-				case val[0] == '\'':
-					comment = " // " + val
-					val = ""
+				c := e.pkg.TypesInfo.Defs[v.Names[i]].(*types.Const)
+				cv, err := parser.ParseExpr("", c.Val().String())
+				if err != nil {
+					panic(err)
+				}
 
-				case strings.HasPrefix(val, "0"):
-					for _, c := range val[1:] {
-						if c < '0' || '9' < c {
-							val = ""
-							break outer
+				// Use orignal Go value if compatible with CUE (octal is okay)
+				if b, ok := cv.(*cueast.BasicLit); ok {
+					if b.Kind == cuetoken.INT && val != "" && val[0] != '\'' {
+						b.Value = val
+					}
+					if b.Value != val {
+						cv.AddComment(internal.NewComment(false, val))
+					}
+				}
+
+				typ := e.pkg.TypesInfo.TypeOf(name)
+				if s := typ.String(); !strings.Contains(s, "untyped") {
+					switch s {
+					case "byte", "string", "error":
+					default:
+						cv = &cueast.BinaryExpr{
+							X:  e.makeType(typ),
+							Op: cuetoken.AND,
+							Y:  cv,
 						}
 					}
-					val = "0o" + val[1:]
 				}
 
-				if val == "" {
-					c := e.pkg.TypesInfo.Defs[v.Names[i]].(*types.Const)
-					val = c.Val().String()
-				}
-
-				fmt.Fprint(e.w, val, comment)
-				e.newLine()
+				f.Value = cv
 			}
 		}
-		e.newLine()
 	}
-	return added
+	return a
 }
 
 func shortTypeName(t types.Type) string {
@@ -702,14 +702,14 @@
 	return t.String()
 }
 
-func (e *extractor) altType(typ types.Type) string {
+func (e *extractor) altType(typ types.Type) cueast.Expr {
 	ptr := types.NewPointer(typ)
 	for _, x := range toTop {
 		i := x.Underlying().(*types.Interface)
 		if types.Implements(typ, i) || types.Implements(ptr, i) {
 			t := shortTypeName(typ)
 			e.logf("    %v implements %s; setting type to _", t, x)
-			return "_"
+			return e.ident("_")
 		}
 	}
 	for _, x := range toString {
@@ -717,16 +717,27 @@
 		if types.Implements(typ, i) || types.Implements(ptr, i) {
 			t := shortTypeName(typ)
 			e.logf("    %v implements %s; setting type to string", t, x)
-			return "string"
+			return e.ident("string")
 		}
 	}
-	return ""
+	return nil
 }
 
-func writeDoc(w io.Writer, g *ast.CommentGroup) {
-	if g == nil {
-		return
+func addDoc(g *ast.CommentGroup, x cueast.Node) bool {
+	doc := makeDoc(g, true)
+	if doc != nil {
+		x.AddComment(doc)
+		return true
 	}
+	return false
+}
+
+func makeDoc(g *ast.CommentGroup, isDoc bool) *cueast.CommentGroup {
+	if g == nil {
+		return nil
+	}
+
+	a := []*cueast.Comment{}
 
 	for _, comment := range g.List {
 		c := comment.Text
@@ -736,7 +747,7 @@
 		switch c[1] {
 		case '/':
 			//-style comment (no newline at the end)
-			_, _ = fmt.Fprintf(w, "//%s\n", c[2:])
+			a = append(a, &cueast.Comment{Text: c})
 
 		case '*':
 			/*-style comment */
@@ -773,28 +784,14 @@
 			// Print lines.
 			for _, l := range lines {
 				if i >= len(l) {
-					_, _ = io.WriteString(w, "//\n")
+					a = append(a, &cueast.Comment{Text: "//"})
 					continue
 				}
-				_, _ = fmt.Fprintf(w, "// %s\n", l[i:])
+				a = append(a, &cueast.Comment{Text: "// " + l[i:]})
 			}
 		}
 	}
-}
-
-func (e *extractor) printDoc(doc *ast.CommentGroup, newline bool) {
-	if doc == nil {
-		return
-	}
-	if newline {
-		e.newLine()
-	}
-	writeDoc(e.w, doc)
-}
-
-func (e *extractor) newLine() {
-	fmt.Fprintln(e.w)
-	fmt.Fprint(e.w, strings.Repeat("    ", e.indent))
+	return &cueast.CommentGroup{Doc: isDoc, List: a}
 }
 
 func supportedType(stack []types.Type, t types.Type) (ok bool) {
@@ -867,27 +864,31 @@
 	return false
 }
 
-func (e *extractor) printField(name string, kind cuetoken.Token, expr types.Type, doc *ast.CommentGroup, newline bool) (typename string) {
-	e.printDoc(doc, newline)
-	colon := ": "
-	switch kind {
-	case cuetoken.ISA:
-		colon = " :: "
-	case cuetoken.OPTION:
-		colon = "?: "
+func (e *extractor) makeField(name string, kind cuetoken.Token, expr types.Type, doc *ast.CommentGroup, newline bool) (f *cueast.Field, typename string) {
+	typ := e.makeType(expr)
+	f = &cueast.Field{
+		Label: e.label(name),
+		Token: kind,
+		Value: typ,
 	}
-	fmt.Fprint(e.w, name, colon)
-	pos := e.w.Len()
-	e.printType(expr)
-	return e.w.String()[pos:]
+	if doc := makeDoc(doc, newline); doc != nil {
+		f.AddComment(doc)
+		cueast.SetRelPos(doc, cuetoken.NewSection)
+	}
+
+	if kind == cuetoken.OPTION {
+		f.Token = cuetoken.COLON
+		f.Optional = cuetoken.Blank.Pos()
+	}
+	b, _ := format.Node(typ)
+	return f, string(b)
 }
 
-func (e *extractor) printType(expr types.Type) {
+func (e *extractor) makeType(expr types.Type) (result cueast.Expr) {
 	if x, ok := expr.(*types.Named); ok {
 		obj := x.Obj()
 		if obj.Pkg() == nil {
-			fmt.Fprint(e.w, "_")
-			return
+			return e.ident("_")
 		}
 		// Check for builtin packages.
 		// TODO: replace these literal types with a reference to the fixed
@@ -895,56 +896,55 @@
 		switch obj.Type().String() {
 		case "time.Time":
 			e.usedInFile["time"] = true
-			fmt.Fprint(e.w, e.pkgNames[obj.Pkg().Path()].name, ".", obj.Name())
-			return
+			ref := e.ident(e.pkgNames[obj.Pkg().Path()].name)
+			return cueast.NewSel(ref, obj.Name())
 
 		case "math/big.Int":
-			fmt.Fprint(e.w, "int")
-			return
+			return e.ident("int")
 
 		default:
 			if !strings.ContainsAny(obj.Pkg().Path(), ".") {
 				// Drop any standard library type if they haven't been handled
 				// above.
-				if s := e.altType(obj.Type()); s != "" {
-					fmt.Fprint(e.w, s)
-					return
+				// TODO: Doc?
+				if s := e.altType(obj.Type()); s != nil {
+					return s
 				}
 			}
 		}
+		result = e.ident(obj.Name())
 		if pkg := obj.Pkg(); pkg != nil {
 			if info := e.pkgNames[pkg.Path()]; info.name != "" {
-				fmt.Fprint(e.w, info.name, ".")
+				result = cueast.NewSel(e.ident(info.name), obj.Name())
 				e.usedPkg(pkg.Path())
 			}
 		}
-		fmt.Fprint(e.w, obj.Name())
 		return
 	}
 
 	switch x := expr.(type) {
 	case *types.Pointer:
-		fmt.Fprintf(e.w, "null | ")
-		e.printType(x.Elem())
+		return &cueast.BinaryExpr{
+			X:  e.ident("null"),
+			Op: cuetoken.OR,
+			Y:  e.makeType(x.Elem()),
+		}
 
 	case *types.Struct:
-		fmt.Fprint(e.w, "{")
-		e.indent++
-		e.printFields(x)
-		e.indent--
-		e.newLine()
-		fmt.Fprint(e.w, "}")
+		st := &cueast.StructLit{
+			Lbrace: cuetoken.Blank.Pos(),
+			Rbrace: cuetoken.Newline.Pos(),
+		}
+		e.addFields(x, st)
+		return st
 
 	case *types.Slice:
 		// TODO: should this be x.Elem().Underlying().String()? One could
 		// argue either way.
 		if x.Elem().String() == "byte" {
-			fmt.Fprint(e.w, "bytes")
-		} else {
-			fmt.Fprint(e.w, "[...")
-			e.printType(x.Elem())
-			fmt.Fprint(e.w, "]")
+			return e.ident("bytes")
 		}
+		return cueast.NewList(&cueast.Ellipsis{Type: e.makeType(x.Elem())})
 
 	case *types.Array:
 		if x.Elem().String() == "byte" {
@@ -953,26 +953,39 @@
 			//     fmt.Fprint(e.w, fmt.Sprintf("=~ '^\C{%d}$'", x.Len())),
 			// but regexp does not support that.
 			// But translate to bytes, instead of [...byte] to be consistent.
-			fmt.Fprint(e.w, "bytes")
+			return e.ident("bytes")
 		} else {
-			fmt.Fprintf(e.w, "%d*[", x.Len())
-			e.printType(x.Elem())
-			fmt.Fprint(e.w, "]")
+			return &cueast.BinaryExpr{
+				X: &cueast.BasicLit{
+					Kind:  cuetoken.INT,
+					Value: strconv.Itoa(int(x.Len())),
+				},
+				Op: cuetoken.MUL,
+				Y:  cueast.NewList(e.makeType(x.Elem())),
+			}
 		}
 
 	case *types.Map:
 		if b, ok := x.Key().Underlying().(*types.Basic); !ok || b.Kind() != types.String {
 			panic(fmt.Sprintf("unsupported map key type %T", x.Key()))
 		}
-		fmt.Fprintf(e.w, "{ [string]: ")
-		e.printType(x.Elem())
-		fmt.Fprintf(e.w, " }")
+
+		f := &cueast.Field{
+			Label: cueast.NewList(e.ident("string")),
+			Value: e.makeType(x.Elem()),
+		}
+		cueast.SetRelPos(f, cuetoken.Blank)
+		return &cueast.StructLit{
+			Lbrace: cuetoken.Blank.Pos(),
+			Elts:   []cueast.Decl{f},
+			Rbrace: cuetoken.Blank.Pos(),
+		}
 
 	case *types.Basic:
-		fmt.Fprint(e.w, x.String())
+		return e.ident(x.String())
 
 	case *types.Interface:
-		fmt.Fprintf(e.w, "_")
+		return e.ident("_")
 
 	default:
 		// record error
@@ -980,7 +993,16 @@
 	}
 }
 
-func (e *extractor) printFields(x *types.Struct) {
+func (e *extractor) addAttr(f *cueast.Field, tag, body string) {
+	s := fmt.Sprintf("@%s(%s)", tag, body)
+	f.Attrs = append(f.Attrs, &cueast.Attribute{Text: s})
+}
+
+func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
+	add := func(x cueast.Decl) {
+		st.Elts = append(st.Elts, x)
+	}
+
 	s := e.orig[x]
 	docs := []*ast.CommentGroup{}
 	for _, f := range s.Fields.List {
@@ -1005,19 +1027,15 @@
 		if f.Anonymous() && e.isInline(x.Tag(i)) {
 			typ := f.Type()
 			if _, ok := typ.(*types.Named); ok {
-				// TODO: strongly consider allowing Expressions for embedded
-				//       values. This ties in with using dots instead of spaces,
-				//       comprehensions, and the ability to generate good
-				//       error messages, so thread carefully.
+				embed := &cueast.EmbedDecl{Expr: e.makeType(typ)}
 				if i > 0 {
-					fmt.Fprintln(e.w)
+					cueast.SetRelPos(embed, cuetoken.NewSection)
 				}
-				fmt.Fprint(e.w, "\n")
-				e.printType(typ)
+				add(embed)
 			} else {
 				switch x := typ.(type) {
 				case *types.Struct:
-					e.printFields(x)
+					e.addFields(x, st)
 				default:
 					panic(fmt.Sprintf("unimplemented embedding for type %T", x))
 				}
@@ -1029,10 +1047,7 @@
 		if name == "-" {
 			continue
 		}
-		if x, _ := cueast.QuoteIdent(name); x != name {
-			name = strconv.Quote(name)
-		}
-		e.newLine()
+		// TODO: check referrers
 		kind := cuetoken.COLON
 		if e.isOptional(tag) {
 			kind = cuetoken.OPTION
@@ -1040,7 +1055,8 @@
 		if _, ok := f.Type().(*types.Pointer); ok {
 			kind = cuetoken.OPTION
 		}
-		cueType := e.printField(name, kind, f.Type(), docs[i], count > 0)
+		field, cueType := e.makeField(name, kind, f.Type(), docs[i], count > 0)
+		add(field)
 
 		// Add field tag to convert back to Go.
 		typeName := f.Type().String()
@@ -1052,17 +1068,17 @@
 
 		// TODO: remove fields in @go attr that are the same as printed?
 		if name != f.Name() || typeName != cueType {
-			fmt.Fprint(e.w, "@go(")
+			buf := &strings.Builder{}
 			if name != f.Name() {
-				fmt.Fprint(e.w, f.Name())
+				buf.WriteString(f.Name())
 			}
 			if typeName != cueType {
 				if strings.ContainsAny(typeName, `#"',()=`) {
 					typeName = strconv.Quote(typeName)
 				}
-				fmt.Fprint(e.w, ",", typeName)
+				fmt.Fprint(buf, ",", typeName)
 			}
-			fmt.Fprintf(e.w, ")")
+			e.addAttr(field, "go", buf.String())
 		}
 
 		// Carry over protobuf field tags with modifications.
@@ -1083,17 +1099,17 @@
 			if len(split) >= 2 {
 				split[0], split[1] = split[1], split[0]
 			}
-			fmt.Fprintf(e.w, " @protobuf(%s)", strings.Join(split, ","))
+			e.addAttr(field, "protobuf", strings.Join(split, ","))
 		}
 
 		// Carry over XML tags.
 		if t := reflect.StructTag(tag).Get("xml"); t != "" {
-			fmt.Fprintf(e.w, " @xml(%s)", t)
+			e.addAttr(field, "xml", t)
 		}
 
 		// Carry over TOML tags.
 		if t := reflect.StructTag(tag).Get("toml"); t != "" {
-			fmt.Fprintf(e.w, " @toml(%s)", t)
+			e.addAttr(field, "toml", t)
 		}
 
 		// TODO: should we in general carry over any unknown tag verbatim?
diff --git a/cmd/cue/cmd/testdata/pkg/cuelang.org/go/cmd/cue/cmd/testdata/code/go/pkg2/pkg2_go_gen.cue b/cmd/cue/cmd/testdata/pkg/cuelang.org/go/cmd/cue/cmd/testdata/code/go/pkg2/pkg2_go_gen.cue
index 41dc9b9..79c99f6 100644
--- a/cmd/cue/cmd/testdata/pkg/cuelang.org/go/cmd/cue/cmd/testdata/code/go/pkg2/pkg2_go_gen.cue
+++ b/cmd/cue/cmd/testdata/pkg/cuelang.org/go/cmd/cue/cmd/testdata/code/go/pkg2/pkg2_go_gen.cue
@@ -5,9 +5,7 @@
 // Package pkgtwo does other stuff.
 package pkgtwo
 
-import (
-	t "time"
-)
+import t "time"
 
 // A Barzer barzes.
 Barzer :: {
diff --git a/doc/tutorial/kubernetes/quick/cue.mod/gen/k8s.io/apimachinery/pkg/watch/watch_go_gen.cue b/doc/tutorial/kubernetes/quick/cue.mod/gen/k8s.io/apimachinery/pkg/watch/watch_go_gen.cue
index 02b6b22..34a7ecd 100644
--- a/doc/tutorial/kubernetes/quick/cue.mod/gen/k8s.io/apimachinery/pkg/watch/watch_go_gen.cue
+++ b/doc/tutorial/kubernetes/quick/cue.mod/gen/k8s.io/apimachinery/pkg/watch/watch_go_gen.cue
@@ -4,9 +4,7 @@
 
 package watch
 
-import (
-	"k8s.io/apimachinery/pkg/runtime"
-)
+import "k8s.io/apimachinery/pkg/runtime"
 
 // Interface can be implemented by anything that knows how to watch and report changes.
 Interface :: _
diff --git a/internal/internal.go b/internal/internal.go
index f4e0ab7..64a48e7 100644
--- a/internal/internal.go
+++ b/internal/internal.go
@@ -20,6 +20,9 @@
 // TODO: refactor packages as to make this package unnecessary.
 
 import (
+	"bufio"
+	"strings"
+
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/token"
 	"github.com/cockroachdb/apd/v2"
@@ -100,3 +103,25 @@
 	}
 	return nil, "", f.Pos()
 }
+
+// NewComment creates a new CommentGroup from the given text.
+// Each line is prefixed with "//" and the last newline is removed.
+// Useful for ASTs generated by code other than the CUE parser.
+func NewComment(isDoc bool, s string) *ast.CommentGroup {
+	if s == "" {
+		return nil
+	}
+	cg := &ast.CommentGroup{Doc: isDoc}
+	if !isDoc {
+		cg.Line = true
+		cg.Position = 10
+	}
+	scanner := bufio.NewScanner(strings.NewReader(s))
+	for scanner.Scan() {
+		cg.List = append(cg.List, &ast.Comment{Text: "// " + scanner.Text()})
+	}
+	if last := len(cg.List) - 1; cg.List[last].Text == "// " {
+		cg.List = cg.List[:last]
+	}
+	return cg
+}