cue/ast/astutil: add Expr to File conversion (ToFile)

The use of ast.File has been a bit awkward. With the
new Sanitize functionality (this package), however,
it comes in handy.

The idea is to move the entire API to a model where
the user would only care about expressions and let
calls like ToFile and Sanitize care about creating a
well-formed file.

Change-Id: Ib034c1074dc4c8c5513b22beb9c4342bae30e974
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6303
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
diff --git a/cue/ast/astutil/file.go b/cue/ast/astutil/file.go
new file mode 100644
index 0000000..e060b71
--- /dev/null
+++ b/cue/ast/astutil/file.go
@@ -0,0 +1,38 @@
+// Copyright 2020 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package astutil
+
+import (
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/token"
+)
+
+// ToFile converts an expression to a File. It will create an import section for
+// any of the identifiers in x that refer to an import and will unshadow
+// references as appropriate.
+func ToFile(x ast.Expr) (*ast.File, error) {
+	var f *ast.File
+	if st, ok := x.(*ast.StructLit); ok {
+		f = &ast.File{Decls: st.Elts}
+	} else {
+		ast.SetRelPos(x, token.NoSpace)
+		f = &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: x}}}
+	}
+
+	if err := Sanitize(f); err != nil {
+		return nil, err
+	}
+	return f, nil
+}
diff --git a/cue/ast/astutil/file_test.go b/cue/ast/astutil/file_test.go
new file mode 100644
index 0000000..a6aeb59
--- /dev/null
+++ b/cue/ast/astutil/file_test.go
@@ -0,0 +1,100 @@
+// Copyright 2020 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package astutil_test
+
+import (
+	"strings"
+	"testing"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/ast/astutil"
+	"cuelang.org/go/cue/format"
+	"cuelang.org/go/cue/token"
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestToFile(t *testing.T) {
+	mat := ast.NewIdent("math")
+	mat.Node = ast.NewImport(nil, "math")
+	pi := ast.NewSel(mat, "Pi")
+
+	testCases := []struct {
+		desc string
+		expr ast.Expr
+		want string
+	}{{
+		desc: "add import",
+		expr: ast.NewBinExpr(token.ADD, ast.NewLit(token.INT, "1"), pi),
+		want: "4.14159265358979323846264",
+	}, {
+		desc: "resolve unresolved within struct",
+		expr: ast.NewStruct(
+			ast.NewIdent("a"), ast.NewString("foo"),
+			ast.NewIdent("b"), ast.NewIdent("a"),
+		),
+		want: `
+a: "foo"
+b: "foo"
+`,
+	}, {
+		desc: "unshadow",
+		expr: func() ast.Expr {
+			ident := ast.NewIdent("a")
+			ref := ast.NewIdent("a")
+			ref.Node = ident
+
+			return ast.NewStruct(
+				ident, ast.NewString("bar"),
+				ast.NewIdent("c"), ast.NewStruct(
+					ast.NewIdent("a"), ast.NewString("foo"),
+					ast.NewIdent("b"), ref, // refers to outer `a`.
+				))
+		}(),
+		want: `
+a: "bar"
+c: {
+	a: "foo"
+	b: "bar"
+}
+`,
+	}}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			f, err := astutil.ToFile(tc.expr)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			var r cue.Runtime
+
+			inst, err := r.CompileFile(f)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			b, err := format.Node(inst.Value().Syntax())
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			got := string(b)
+			want := strings.TrimLeft(tc.want, "\n")
+			if got != want {
+				t.Error(cmp.Diff(got, want))
+			}
+		})
+	}
+}