cmd/cue: make fmt rewrite deprecated cue

- rewrite string labels to identifiers
- remove block comments

Issue #87

Change-Id: I523814add1081abbd677ae74bd1556c5beb4abd4
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3190
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/fix.go b/cmd/cue/cmd/fix.go
new file mode 100644
index 0000000..a3fd8a2
--- /dev/null
+++ b/cmd/cue/cmd/fix.go
@@ -0,0 +1,147 @@
+// Copyright 2019 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 cmd
+
+import (
+	"strconv"
+	"strings"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/ast/astutil"
+	"cuelang.org/go/cue/parser"
+	"cuelang.org/go/cue/token"
+)
+
+func fix(f *ast.File) *ast.File {
+	// Rewrite block comments to regular comments.
+	ast.Walk(f, func(n ast.Node) bool {
+		switch x := n.(type) {
+		case *ast.CommentGroup:
+			comments := []*ast.Comment{}
+			for _, c := range x.List {
+				s := c.Text
+				if !strings.HasPrefix(s, "/*") || !strings.HasSuffix(s, "*/") {
+					comments = append(comments, c)
+					continue
+				}
+				if x.Position > 0 {
+					// Moving to the end doesn't work, as it still
+					// may inject at a false line break position.
+					x.Position = 0
+					x.Doc = true
+				}
+				s = strings.TrimSpace(s[2 : len(s)-2])
+				for _, s := range strings.Split(s, "\n") {
+					for i := 0; i < 3; i++ {
+						if strings.HasPrefix(s, " ") || strings.HasPrefix(s, "*") {
+							s = s[1:]
+						}
+					}
+					comments = append(comments, &ast.Comment{Text: "// " + s})
+				}
+			}
+			x.List = comments
+			return false
+		}
+		return true
+	}, nil)
+
+	// Rewrite strings fields that are referenced.
+	referred := map[ast.Node]string{}
+	ast.Walk(f, func(n ast.Node) bool {
+		if i, ok := n.(*ast.Ident); ok {
+			str, err := ast.ParseIdent(i)
+			if err != nil {
+				return false
+			}
+			referred[i.Node] = str
+		}
+		return true
+	}, nil)
+
+	f = astutil.Apply(f, func(c astutil.Cursor) bool {
+		n := c.Node()
+		switch x := n.(type) {
+		case *ast.Field:
+			m, ok := referred[x.Value]
+			if !ok {
+				break
+			}
+			b, ok := x.Label.(*ast.BasicLit)
+			if !ok || b.Kind != token.STRING {
+				break
+			}
+			str, err := strconv.Unquote(b.Value)
+			if err != nil || str != m {
+				break
+			}
+			str, err = ast.QuoteIdent(str)
+			if err != nil {
+				return false
+			}
+			x.Label = astutil.CopyMeta(ast.NewIdent(str), x.Label).(ast.Label)
+		}
+		return true
+	}, nil).(*ast.File)
+
+	// Rewrite slice expression.
+	f = astutil.Apply(f, func(c astutil.Cursor) bool {
+		n := c.Node()
+		getVal := func(n ast.Expr) ast.Expr {
+			if n == nil {
+				return nil
+			}
+			if id, ok := n.(*ast.Ident); ok && id.Name == "_" {
+				return nil
+			}
+			return n
+		}
+		switch x := n.(type) {
+		case *ast.SliceExpr:
+			ast.SetRelPos(x.X, token.NoRelPos)
+
+			lo := getVal(x.Low)
+			hi := getVal(x.High)
+			if lo == nil { // a[:j]
+				lo = mustParseExpr("0")
+				astutil.CopyMeta(lo, x.Low)
+			}
+			if hi == nil { // a[i:]
+				hi = &ast.CallExpr{
+					Fun:  ast.NewIdent("len"),
+					Args: []ast.Expr{x.X},
+				}
+				astutil.CopyMeta(lo, x.High)
+			}
+			if pkg := c.Import("list"); pkg != nil {
+				c.Replace(&ast.CallExpr{
+					Fun:  &ast.SelectorExpr{X: pkg, Sel: ast.NewIdent("Slice")},
+					Args: []ast.Expr{x.X, lo, hi},
+				})
+			}
+		}
+		return true
+	}, nil).(*ast.File)
+
+	return f
+}
+
+func mustParseExpr(expr string) ast.Expr {
+	ex, err := parser.ParseExpr("fix", expr)
+	if err != nil {
+		panic(err)
+	}
+	return ex
+}
diff --git a/cmd/cue/cmd/fix_test.go b/cmd/cue/cmd/fix_test.go
new file mode 100644
index 0000000..cb70740
--- /dev/null
+++ b/cmd/cue/cmd/fix_test.go
@@ -0,0 +1,175 @@
+// Copyright 2019 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 cmd
+
+import (
+	"testing"
+
+	"cuelang.org/go/cue/format"
+	"cuelang.org/go/cue/parser"
+)
+
+func TestFix(t *testing.T) {
+	testCases := []struct {
+		name string
+		in   string
+		out  string
+	}{{
+		name: "referenced string fields",
+		in: `package foo
+
+"foo": 3
+"foo-bar": 2
+"baz": ` + "`foo-bar`" + `
+
+a: {
+	"qux": 3
+	"qux-quux": qux
+	"qaz": ` + "`qux-quux`" + `
+}
+`,
+		out: `package foo
+
+"foo":     3
+` + "`foo-bar`" + `: 2
+"baz":     ` + "`foo-bar`" + `
+
+a: {
+	qux:        3
+	` + "`qux-quux`" + `: qux
+	"qaz":      ` + "`qux-quux`" + `
+}
+`,
+	}, {
+		name: "comprehensions",
+		in: `
+package fix
+
+"\(k)": v for k, v in src
+
+"\(k)": v <-
+ for k, v in src
+
+/* foo
+   bar
+ */
+
+a: 3 + /* foo */ 5
+	 `,
+		out: `package fix
+
+for k, v in src {
+	"\(k)": v
+}
+// foo
+// bar
+for k, v in src {
+	"\(k)": v
+}
+
+a:
+	// foo
+	3 + 5
+`,
+	}, {
+		name: "comments",
+		in: `package foo
+
+a: /* b */ 3 + 5
+a: 3 /* c */ + 5
+a: 3 + /* d */ 5
+a: 3 + 5 /* e
+f */
+`,
+		out: `package foo
+
+// b
+a: 3 + 5
+a:
+	// c
+	3 + 5
+a:
+	// d
+	3 + 5
+// e
+// f
+a: 3 + 5
+`,
+	}, {
+		name: "slice",
+		in: `package foo
+
+// keep comment
+l[3:4] // and this one
+
+a: len(l[3:4])
+b: len(l[a:_])
+c: len(l[_:x])
+d: len(l[_:_])
+`,
+		out: `package foo
+
+import list6c6973 "list"
+
+// keep comment
+list6c6973.Slice(l, 3, 4)// and this one
+
+a: len(list6c6973.Slice(l, 3, 4))
+b: len(list6c6973.Slice(l, a, len(l)))
+c: len(list6c6973.Slice(l, 0, x))
+d: len(list6c6973.Slice(l, 0, len(l)))
+`,
+	}, {
+		name: "slice2",
+		in: `package foo
+
+import "list"
+
+a: list.Contains("foo")
+b: len(l[_:_])
+`,
+		out: `package foo
+
+import (
+	"list"
+	list6c6973 "list"
+)
+
+a: list.Contains("foo")
+b: len(list6c6973.Slice(l, 0, len(l)))
+`,
+	}}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			f, err := parser.ParseFile("", tc.in, parser.ParseComments)
+			if err != nil {
+				t.Fatal(err)
+			}
+			n := fix(f)
+			b, err := format.Node(n)
+			if err != nil {
+				t.Fatal(err)
+			}
+			got := string(b)
+			if got != tc.out {
+				t.Errorf("got %v; want %v", got, tc.out)
+			}
+			_, err = parser.ParseFile("rewritten", got, parser.ParseComments)
+			if err != nil {
+				t.Fatal(err)
+			}
+		})
+	}
+}
diff --git a/cmd/cue/cmd/fmt.go b/cmd/cue/cmd/fmt.go
index 93298a2..c569f0f 100644
--- a/cmd/cue/cmd/fmt.go
+++ b/cmd/cue/cmd/fmt.go
@@ -20,6 +20,7 @@
 
 	"cuelang.org/go/cue/format"
 	"cuelang.org/go/cue/load"
+	"cuelang.org/go/cue/parser"
 	"github.com/spf13/cobra"
 )
 
@@ -53,7 +54,13 @@
 						opts = append(opts, format.Simplify())
 					}
 
-					b, err = format.Source(b, opts...)
+					f, err := parser.ParseFile(fullpath, b, parser.ParseComments)
+					if err != nil {
+						return err
+					}
+					n := fix(f)
+
+					b, err = format.Node(n, opts...)
 					if err != nil {
 						return err
 					}
diff --git a/cue/ast/astutil/util.go b/cue/ast/astutil/util.go
index 2a237b3..cbd0629 100644
--- a/cue/ast/astutil/util.go
+++ b/cue/ast/astutil/util.go
@@ -24,3 +24,22 @@
 	}
 	ast.SetComments(to, from.Comments())
 }
+
+// CopyPosition sets the position of one node to another.
+func CopyPosition(to, from ast.Node) {
+	if from == nil {
+		return
+	}
+	ast.SetPos(to, from.Pos())
+}
+
+// CopyMeta copies comments and position information from one node to another.
+// It returns the destination node.
+func CopyMeta(to, from ast.Node) ast.Node {
+	if from == nil {
+		return to
+	}
+	ast.SetComments(to, from.Comments())
+	ast.SetPos(to, from.Pos())
+	return to
+}
diff --git a/cue/ast/astutil/walk.go b/cue/ast/astutil/walk.go
index e115c32..6a24234 100644
--- a/cue/ast/astutil/walk.go
+++ b/cue/ast/astutil/walk.go
@@ -175,9 +175,7 @@
 		walkDeclList(v, n.Decls)
 
 	case *ast.Package:
-		if n.Name != nil {
-			walk(v, n.Name)
-		}
+		// The package identifier isn't really an identifier. Skip it.
 
 	case *ast.ListComprehension:
 		walk(v, n.Expr)
diff --git a/cue/format/format.go b/cue/format/format.go
index f1274e1..e83a98c 100644
--- a/cue/format/format.go
+++ b/cue/format/format.go
@@ -336,6 +336,9 @@
 		f.Print(c)
 		if isEnd {
 			f.Print(newline)
+			if cg.Doc {
+				f.Print(nooverride)
+			}
 		}
 	}
 }
diff --git a/internal/third_party/yaml/decode.go b/internal/third_party/yaml/decode.go
index 00a7e19..749374e 100644
--- a/internal/third_party/yaml/decode.go
+++ b/internal/third_party/yaml/decode.go
@@ -323,6 +323,7 @@
 
 func (d *decoder) attachDocComments(m yaml_mark_t, pos int8, expr ast.Node) {
 	comments := []*ast.Comment{}
+	line := 0
 	for len(d.p.parser.comments) > 0 {
 		c := d.p.parser.comments[0]
 		if c.mark.index >= m.index {
@@ -333,10 +334,11 @@
 			Text:  "//" + c.text[1:],
 		})
 		d.p.parser.comments = d.p.parser.comments[1:]
+		line = c.mark.line
 	}
 	if len(comments) > 0 {
 		expr.AddComment(&ast.CommentGroup{
-			Doc:      pos == 0,
+			Doc:      pos == 0 && line+1 == m.line,
 			Position: pos,
 			List:     comments,
 		})