cue/format: allow invalid identifiers as labels

"Manually" generated AST's may contain identifiers
that are nor properly formatted. When used as a label, instead of returning an error, convert it to a string label.

Change-Id: I1498636b484cfed70380620cae14fffcb54e94e5
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2367
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/format/format_test.go b/cue/format/format_test.go
index 6316c7b..2e5b66f 100644
--- a/cue/format/format_test.go
+++ b/cue/format/format_test.go
@@ -388,6 +388,25 @@
 	}
 }
 
+func TestIncorrectIdent(t *testing.T) {
+	testCases := []struct {
+		ident string
+		out   string
+	}{
+		{"foo", "foo"},
+		{"a.b.c", `"a.b.c"`},
+		{"for", "for"},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.ident, func(t *testing.T) {
+			b, _ := Node(&ast.Field{Label: ast.NewIdent(tc.ident), Value: ast.NewIdent("A")})
+			if got, want := string(b), tc.out+`: A`; got != want {
+				t.Errorf("got %q; want %q", got, want)
+			}
+		})
+	}
+}
+
 // TextX is a skeleton test that can be filled in for debugging one-off cases.
 // Do not remove.
 func TestX(t *testing.T) {
diff --git a/cue/format/node.go b/cue/format/node.go
index 8cecc49..e373f4f 100644
--- a/cue/format/node.go
+++ b/cue/format/node.go
@@ -20,7 +20,7 @@
 	"strings"
 
 	"cuelang.org/go/cue/ast"
-	"cuelang.org/go/cue/parser"
+	"cuelang.org/go/cue/scanner"
 	"cuelang.org/go/cue/token"
 )
 
@@ -287,18 +287,34 @@
 	f.print(newline)
 }
 
+func isValidIdent(ident string) bool {
+	var scan scanner.Scanner
+	scan.Init(token.NewFile("check", -1, len(ident)), []byte(ident), nil, 0)
+
+	_, tok, lit := scan.Scan()
+	if tok == token.IDENT || tok.IsKeyword() {
+		return lit == ident
+	}
+	return false
+}
+
 func (f *formatter) label(l ast.Label, optional bool) {
 	switch n := l.(type) {
 	case *ast.Ident:
-		f.print(n.NamePos, n)
+		// Escape an identifier that has invalid characters. This may happen,
+		// if the AST is not generated by the parser.
+		if isValidIdent(n.Name) {
+			f.print(n.NamePos, n)
+		} else {
+			f.print(n.NamePos, strconv.Quote(n.Name))
+		}
 
 	case *ast.BasicLit:
 		if f.cfg.simplify && n.Kind == token.STRING && len(n.Value) > 2 {
 			s := n.Value
 			unquoted, err := strconv.Unquote(s)
 			if err == nil {
-				e, _ := parser.ParseExpr("check", unquoted)
-				if _, ok := e.(*ast.Ident); ok {
+				if isValidIdent(unquoted) {
 					f.print(n.ValuePos, unquoted)
 					break
 				}