cue: add API for merged doc comments

field doc for Value
pkg docs for an Instance

Fixes #36

Change-Id: I45bf4604aafa235bdd987a449184b99b2a084db7
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2020
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/ast.go b/cue/ast.go
index 1485a2a..7f00b4e 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -50,6 +50,13 @@
 type astVisitor struct {
 	*astState
 	object *structLit
+	// For single line fields, the doc comment is applied to the inner-most
+	// field value.
+	//
+	//   // This comment is for bar.
+	//   foo bar: value
+	//
+	doc *docNode
 
 	inSelector int
 }
@@ -187,6 +194,10 @@
 			astState: v.astState,
 			object:   obj,
 		}
+		passDoc := len(n.Elts) == 1 && !n.Lbrace.IsValid() && v.doc != nil
+		if passDoc {
+			v1.doc = v.doc
+		}
 		for _, e := range n.Elts {
 			switch x := e.(type) {
 			case *ast.EmitDecl:
@@ -198,6 +209,9 @@
 				v1.walk(x)
 			}
 		}
+		if passDoc {
+			v.doc = v1.doc // signal usage of document back to parent.
+		}
 		value = obj
 
 	case *ast.ComprehensionDecl:
@@ -274,7 +288,7 @@
 
 			sig := &params{}
 			sig.add(f, &basicType{newNode(n.Label), stringKind})
-			template := &lambdaExpr{newExpr(n.Value), sig, nil}
+			template := &lambdaExpr{newNode(n), sig, nil}
 
 			v.setScope(n, template)
 			template.value = v.walk(n.Value)
@@ -298,7 +312,17 @@
 				return v.error(n.Label, "invalid field name: %v", n.Label)
 			}
 			if f != 0 {
-				v.object.insertValue(v.ctx(), f, opt, v.walk(n.Value), attrs)
+				var leftOverDoc *docNode
+				for _, c := range n.Comments() {
+					if c.Position == 0 {
+						leftOverDoc = v.doc
+						v.doc = &docNode{n: n}
+						break
+					}
+				}
+				val := v.walk(n.Value)
+				v.object.insertValue(v.ctx(), f, opt, val, attrs, v.doc)
+				v.doc = leftOverDoc
 			}
 
 		default:
diff --git a/cue/binop.go b/cue/binop.go
index e9bbd43..452aa64 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -543,7 +543,7 @@
 	for _, a := range x.arcs {
 		cp := ctx.copy(a.v)
 		obj.arcs = append(obj.arcs,
-			arc{a.feature, a.optional, cp, nil, a.attrs})
+			arc{a.feature, a.optional, cp, nil, a.attrs, a.docs})
 	}
 outer:
 	for _, a := range y.arcs {
@@ -554,6 +554,7 @@
 				obj.arcs[i].v = v
 				obj.arcs[i].cache = nil
 				obj.arcs[i].optional = a.optional && b.optional
+				obj.arcs[i].docs = mergeDocs(a.docs, b.docs)
 				attrs, err := unifyAttrs(ctx, src, a.attrs, b.attrs)
 				if err != nil {
 					return err
diff --git a/cue/build_test.go b/cue/build_test.go
index e5d4f30..fb58eb7 100644
--- a/cue/build_test.go
+++ b/cue/build_test.go
@@ -21,6 +21,7 @@
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/parser"
 	"cuelang.org/go/cue/token"
 )
 
@@ -181,7 +182,7 @@
 
 func makeInstances(insts []*bimport) (instances []*build.Instance) {
 	b := builder{
-		ctxt:    build.NewContext(),
+		ctxt:    build.NewContext(build.ParseOptions(parser.ParseComments)),
 		imports: map[string]*bimport{},
 	}
 	for _, bi := range insts {
diff --git a/cue/builtin.go b/cue/builtin.go
index a2dce51..b0df1ef 100644
--- a/cue/builtin.go
+++ b/cue/builtin.go
@@ -87,7 +87,7 @@
 		for _, a := range pkg.arcs {
 			// Discard option status and attributes at top level.
 			// TODO: filter on capitalized fields?
-			obj.insertValue(ctx, a.feature, false, a.v, nil)
+			obj.insertValue(ctx, a.feature, false, a.v, nil, a.docs)
 		}
 	}
 
diff --git a/cue/instance.go b/cue/instance.go
index 452d5ff..62c5f40 100644
--- a/cue/instance.go
+++ b/cue/instance.go
@@ -15,6 +15,8 @@
 package cue
 
 import (
+	"strings"
+
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/build"
 	"cuelang.org/go/internal"
@@ -116,6 +118,29 @@
 	return v.walk(expr).evalPartial(ctx)
 }
 
+// Doc returns the package comments for this instance.
+func (inst *Instance) Doc() []*ast.CommentGroup {
+	var docs []*ast.CommentGroup
+	if inst.inst == nil {
+		return nil
+	}
+	for _, f := range inst.inst.Files {
+		if strings.HasPrefix(f.Filename, inst.Dir) {
+			continue
+		}
+		var cg *ast.CommentGroup
+		for _, c := range f.Comments() {
+			if c.Position == 0 {
+				cg = c
+			}
+		}
+		if cg != nil {
+			docs = append(docs, cg)
+		}
+	}
+	return docs
+}
+
 // Value returns the root value of the configuration. If the configuration
 // defines in emit value, it will be that value. Otherwise it will be all
 // top-level values.
diff --git a/cue/strip.go b/cue/strip.go
index 22defda..54a7a5a 100644
--- a/cue/strip.go
+++ b/cue/strip.go
@@ -91,6 +91,7 @@
 			if err != nil {
 				return err
 			}
+			a.docs = mergeDocs(a.docs, arcs[i].docs)
 		}
 		if len(values) == 1 {
 			arcs[k] = a
diff --git a/cue/types.go b/cue/types.go
index 90b9510..a44aa61 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -687,6 +687,15 @@
 // 	return nil
 // }
 
+// Doc returns all documentation comments associated with the field from which
+// the current value originates.
+func (v Value) Doc() []*ast.CommentGroup {
+	if v.path == nil {
+		return nil
+	}
+	return v.path.docs.appendDocs(nil)
+}
+
 // Split returns a list of values from which v originated such that
 // the unification of all these values equals v and for all returned values
 // Source returns a non-nil value.
diff --git a/cue/types_test.go b/cue/types_test.go
index 06d92fc..36e6acc 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -24,6 +24,7 @@
 	"strings"
 	"testing"
 
+	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
 	"github.com/google/go-cmp/cmp"
 )
@@ -1337,6 +1338,117 @@
 		})
 	}
 }
+
+func TestValueDoc(t *testing.T) {
+	const config = `
+	// foobar defines at least foo.
+	package foobar
+
+	// A Foo fooses stuff.
+	Foo: {
+		// field1 is an int.
+		field1: int
+
+		field2: int
+
+		// duplicate field comment
+		dup3: int
+	}
+
+	// foos are instances of Foo.
+	foos <foo>: Foo
+
+	// My first little foo.
+	foos MyFoo: {
+		// local field comment.
+		field1: 0
+
+		// Dangling comment.
+
+		// other field comment.
+		field2: 1
+
+		// duplicate field comment
+		dup3: int
+	}
+
+	bar: {
+		// comment from bar on field 1
+		field1: int
+		// comment from bar on field 2
+		field2: int // don't include this
+	}
+
+	baz: bar & {
+		// comment from baz on field 1
+		field1: int
+		field2: int
+	}
+	`
+	testCases := []struct {
+		path string
+		doc  string
+	}{{
+		path: "foos",
+		doc:  "foos are instances of Foo.\n",
+	}, {
+		path: "foos MyFoo",
+		doc:  "My first little foo.\n",
+	}, {
+		path: "foos MyFoo field1",
+		doc: `field1 is an int.
+
+local field comment.
+`,
+	}, {
+		path: "foos MyFoo field2",
+		doc:  "other field comment.\n",
+	}, {
+		path: "foos MyFoo dup3",
+		doc: `duplicate field comment
+
+duplicate field comment
+`,
+	}, {
+		path: "bar field1",
+		doc:  "comment from bar on field 1\n",
+	}, {
+		path: "baz field1",
+		doc: `comment from baz on field 1
+
+comment from bar on field 1
+`,
+	}, {
+		path: "baz field2",
+		doc:  "comment from bar on field 2\n",
+	}}
+	inst := getInstance(t, config)
+	for _, tc := range testCases {
+		t.Run("field:"+tc.path, func(t *testing.T) {
+			v := inst.Value().Lookup(strings.Split(tc.path, " ")...)
+			doc := docStr(v.Doc())
+			if doc != tc.doc {
+				t.Errorf("doc: got:\n%vwant:\n%v", doc, tc.doc)
+			}
+		})
+	}
+	want := "foobar defines at least foo.\n"
+	if got := docStr(inst.Doc()); got != want {
+		t.Errorf("pkg: got:\n%vwant:\n%v", got, want)
+	}
+}
+
+func docStr(docs []*ast.CommentGroup) string {
+	doc := ""
+	for _, d := range docs {
+		if doc != "" {
+			doc += "\n"
+		}
+		doc += d.Text()
+	}
+	return doc
+}
+
 func TestMashalJSON(t *testing.T) {
 	testCases := []struct {
 		value string
diff --git a/cue/value.go b/cue/value.go
index 67944a2..dfa5a10 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -609,7 +609,7 @@
 	}
 	arcs := make([]arc, len(a))
 	for i, a := range a {
-		arcs[i] = arc{feature: label(i), v: a.v}
+		arcs[i] = arc{feature: label(i), v: a.v, docs: a.docs}
 	}
 	s := &structLit{baseValue: x.baseValue, arcs: arcs}
 	return &list{baseValue: x.baseValue, elem: s, typ: x.typ, len: max}
@@ -694,7 +694,8 @@
 		v := x.arcs[i].v.evalPartial(ctx)
 		ctx.evalStack = popped
 
-		v = x.applyTemplate(ctx, i, v)
+		var doc *ast.Field
+		v, doc = x.applyTemplate(ctx, i, v)
 
 		if (len(ctx.evalStack) > 0 && ctx.cycleErr) || cycleError(v) != nil {
 			// Don't cache while we're in a evaluation cycle as it will cache
@@ -710,6 +711,9 @@
 		ctx.cycleErr = false
 
 		x.arcs[i].cache = v
+		if doc != nil {
+			x.arcs[i].docs = &docNode{n: doc, left: x.arcs[i].docs}
+		}
 		if len(ctx.evalStack) == 0 {
 			if err := ctx.processDelayedConstraints(); err != nil {
 				x.arcs[i].cache = err
@@ -795,6 +799,7 @@
 				} else {
 					x.arcs[i].v = mkBin(ctx, x.Pos(), opUnify, a.v, na.v)
 					x.arcs[i].optional = x.arcs[i].optional && optional
+					x.arcs[i].docs = mergeDocs(na.docs, a.docs)
 				}
 				continue outer
 			}
@@ -805,18 +810,21 @@
 	return x
 }
 
-func (x *structLit) applyTemplate(ctx *context, i int, v evaluated) evaluated {
+func (x *structLit) applyTemplate(ctx *context, i int, v evaluated) (evaluated, *ast.Field) {
 	if x.template != nil {
 		fn, err := evalLambda(ctx, x.template)
 		if err != nil {
-			return err
+			return err, nil
 		}
 		name := ctx.labelStr(x.arcs[i].feature)
 		arg := &stringLit{x.baseValue, name}
 		w := fn.call(ctx, x, arg).evalPartial(ctx)
 		v = binOp(ctx, x, opUnify, v, w)
+
+		f, _ := x.template.base().syntax().(*ast.Field)
+		return v, f
 	}
-	return v
+	return v, nil
 }
 
 // A label is a canonicalized feature name.
@@ -840,6 +848,46 @@
 	v     value
 	cache evaluated // also used as newValue during unification.
 	attrs *attributes
+	docs  *docNode
+}
+
+type docNode struct {
+	n     *ast.Field
+	left  *docNode
+	right *docNode
+}
+
+func (d *docNode) appendDocs(docs []*ast.CommentGroup) []*ast.CommentGroup {
+	if d == nil {
+		return docs
+	}
+	docs = d.left.appendDocs(docs)
+	if d.n != nil {
+		docs = appendDocComments(docs, d.n)
+		docs = appendDocComments(docs, d.n.Label)
+	}
+	docs = d.right.appendDocs(docs)
+	return docs
+}
+
+func appendDocComments(docs []*ast.CommentGroup, n ast.Node) []*ast.CommentGroup {
+	for _, c := range n.Comments() {
+		if c.Position == 0 {
+			docs = append(docs, c)
+		}
+	}
+	return docs
+}
+
+func mergeDocs(a, b *docNode) *docNode {
+	if a == b || a == nil {
+		return b
+	}
+	if b == nil {
+		return b
+	}
+	// TODO: filter out duplicates?
+	return &docNode{nil, a, b}
 }
 
 func (a *arc) val() evaluated {
@@ -851,15 +899,8 @@
 	a.cache = nil
 }
 
-type arcInfo struct {
-	hidden bool
-	tags   []string // name:string
-}
-
-var hiddenArc = &arcInfo{hidden: true}
-
 // insertValue is used during initialization but never during evaluation.
-func (x *structLit) insertValue(ctx *context, f label, optional bool, value value, a *attributes) {
+func (x *structLit) insertValue(ctx *context, f label, optional bool, value value, a *attributes, docs *docNode) {
 	for i, p := range x.arcs {
 		if f != p.feature {
 			continue
@@ -870,7 +911,7 @@
 		x.arcs[i].optional = x.arcs[i].optional && optional
 		return
 	}
-	x.arcs = append(x.arcs, arc{f, optional, value, nil, a})
+	x.arcs = append(x.arcs, arc{f, optional, value, nil, a, docs})
 	sort.Stable(x)
 }