internal/core/export: support package and struct attributes

Fixes #665

Change-Id: Id2ceec41ff3fcbedb22e74a488b1909da934287f
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/8342
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/testdata/script/def_jsonschema.txt b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
index c1515e8..fe0d7b3 100644
--- a/cmd/cue/cmd/testdata/script/def_jsonschema.txt
+++ b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
@@ -22,6 +22,10 @@
 import "strings"
 
 #Person: {
+	// Person
+	@jsonschema(schema="http://json-schema.org/draft-07/schema#")
+	@jsonschema(id="https://example.com/person.schema.json")
+
 	// The person's first name.
 	firstName?: string
 
diff --git a/internal/core/export/expr.go b/internal/core/export/expr.go
index 48467b5..97c63db 100644
--- a/internal/core/export/expr.go
+++ b/internal/core/export/expr.go
@@ -88,6 +88,7 @@
 		exporter: x,
 		values:   &adt.Vertex{},
 		fields:   map[adt.Feature]field{},
+		attrs:    []*ast.Attribute{},
 	}
 
 	_, saved := e.pushFrame(orig)
@@ -101,6 +102,10 @@
 
 	s := x.top().scope
 
+	for _, a := range e.attrs {
+		s.Elts = append(s.Elts, a)
+	}
+
 	// Unify values only for one level.
 	if len(e.values.Conjuncts) > 0 {
 		e.values.Finalize(e.ctx)
@@ -133,7 +138,7 @@
 		switch len(e.exprs) {
 		case 0:
 			if len(e.structs) > 0 {
-				return ast.NewStruct()
+				return s
 			}
 			return ast.NewIdent("_")
 		case 1:
@@ -207,6 +212,7 @@
 	exprs       []ast.Expr
 	structs     []*adt.StructInfo
 	fields      map[adt.Feature]field
+	attrs       []*ast.Attribute
 	hasEllipsis bool
 }
 
@@ -237,6 +243,10 @@
 	case *adt.StructLit:
 		e.top().upCount++
 
+		if e.cfg.ShowAttributes {
+			e.attrs = extractDeclAttrs(e.attrs, x.Src)
+		}
+
 		// Only add if it only has no bulk fields or elipsis.
 		if isComplexStruct(x) {
 			_, saved := e.pushFrame(nil)
diff --git a/internal/core/export/extract.go b/internal/core/export/extract.go
index b6ccf59..4090fd9 100644
--- a/internal/core/export/extract.go
+++ b/internal/core/export/extract.go
@@ -17,6 +17,7 @@
 import (
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal"
 	"cuelang.org/go/internal/core/adt"
 )
 
@@ -151,6 +152,34 @@
 	return attrs
 }
 
+func ExtractDeclAttrs(a []adt.Conjunct) (attrs []*ast.Attribute) {
+	for _, c := range a {
+		attrs = extractDeclAttrs(attrs, c.Expr().Source())
+	}
+	return attrs
+}
+
+func extractDeclAttrs(attrs []*ast.Attribute, n ast.Node) []*ast.Attribute {
+	switch x := n.(type) {
+	case nil:
+	case *ast.File:
+		info := internal.GetPackageInfo(x)
+		attrs = appendDeclAttrs(attrs, x.Decls[info.Index:])
+	case *ast.StructLit:
+		attrs = appendDeclAttrs(attrs, x.Elts)
+	}
+	return attrs
+}
+
+func appendDeclAttrs(a []*ast.Attribute, decls []ast.Decl) []*ast.Attribute {
+	for _, d := range decls {
+		if attr, ok := d.(*ast.Attribute); ok && !containsAttr(a, attr) {
+			a = append(a, attr)
+		}
+	}
+	return a
+}
+
 func containsAttr(a []*ast.Attribute, x *ast.Attribute) bool {
 	for _, e := range a {
 		if e.Text == x.Text {
diff --git a/internal/core/export/testdata/adt.txtar b/internal/core/export/testdata/adt.txtar
index 8668310..5b701fe 100644
--- a/internal/core/export/testdata/adt.txtar
+++ b/internal/core/export/testdata/adt.txtar
@@ -1,6 +1,8 @@
 -- in.cue --
 import mystrings "strings"
 
+@foo(bar)
+
 p1: [X=string]: name: X
 
 d1: "foo\(bar)": int
@@ -254,6 +256,7 @@
 _|_ // e3: index out of range [2] with length 2
 == All
 {
+	@foo(bar)
 	p1: {}
 	d1: {
 		foobar: int
diff --git a/internal/core/export/testdata/attrs.txtar b/internal/core/export/testdata/attrs.txtar
new file mode 100644
index 0000000..665335a
--- /dev/null
+++ b/internal/core/export/testdata/attrs.txtar
@@ -0,0 +1,84 @@
+-- a.cue --
+@package("foo")
+
+package bar
+
+@file("foo")
+
+a: {
+    @decl(1)
+    @decl(2)
+} @field(2)
+
+a: {
+    @decl(1)
+    @decl(3)
+
+} @field(1) @field(4)
+
+a: {} @field(1) @field(3)
+
+-- b.cue --
+@package("b")
+
+package bar
+
+@file("bar")
+
+a: {
+    @decl(5)
+} @field(5)
+
+
+-- out/definition --
+package bar
+
+@file("foo")
+
+@file("bar")
+a: {
+	@decl(1)
+	@decl(2)
+	@decl(3)
+	@decl(5)
+} @field(2) @field(1) @field(4) @field(3) @field(5)
+-- out/doc --
+[]
+[a]
+-- out/value --
+== Simplified
+{
+	a: {}
+}
+== Raw
+{
+	a: {}
+}
+== Final
+{
+	a: {}
+}
+== All
+{
+	@file("foo")
+
+	@file("bar")
+	a: {
+		@decl(1)
+		@decl(2)
+		@decl(3)
+		@decl(5)
+	} @field(2) @field(1) @field(4) @field(3) @field(5)
+}
+== Eval
+{
+	@file("foo")
+
+	@file("bar")
+	a: {
+		@decl(1)
+		@decl(2)
+		@decl(3)
+		@decl(5)
+	} @field(2) @field(1) @field(4) @field(3) @field(5)
+}
diff --git a/internal/core/export/value.go b/internal/core/export/value.go
index fd5ff07..720c3d7 100644
--- a/internal/core/export/value.go
+++ b/internal/core/export/value.go
@@ -370,6 +370,12 @@
 		e.addEmbed(e.value(x))
 	}
 
+	if e.cfg.ShowAttributes {
+		for _, a := range ExtractDeclAttrs(v.Conjuncts) {
+			s.Elts = append(s.Elts, a)
+		}
+	}
+
 	p := e.cfg
 	for _, label := range VertexFeatures(v) {
 		show := false
diff --git a/internal/internal.go b/internal/internal.go
index 25fdb69..b6c7d89 100644
--- a/internal/internal.go
+++ b/internal/internal.go
@@ -103,8 +103,14 @@
 	return elts, e
 }
 
-func PackageInfo(f *ast.File) (p *ast.Package, name string, tok token.Pos) {
-	for _, d := range f.Decls {
+type PkgInfo struct {
+	Package *ast.Package
+	Index   int // position in File.Decls
+	Name    string
+}
+
+func GetPackageInfo(f *ast.File) PkgInfo {
+	for i, d := range f.Decls {
 		switch x := d.(type) {
 		case *ast.CommentGroup:
 		case *ast.Attribute:
@@ -112,9 +118,18 @@
 			if x.Name == nil {
 				break
 			}
-			return x, x.Name.Name, x.Name.Pos()
+			return PkgInfo{x, i, x.Name.Name}
 		}
 	}
+	return PkgInfo{}
+}
+
+// Deprecated: use GetPackageInfo
+func PackageInfo(f *ast.File) (p *ast.Package, name string, tok token.Pos) {
+	x := GetPackageInfo(f)
+	if p := x.Package; p != nil {
+		return p, x.Name, p.Name.Pos()
+	}
 	return nil, "", f.Pos()
 }