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 := ¶ms{}
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)
}