cue/ast: add back LabelName

Copied from internal, but now with better
semantics and preparing for not allowing
string labels to bind.

Ported most uses of internal.LabelName

Change-Id: I35b178a2c03073a6a6df1f78e0bfe8eae7b882ad
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3261
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/trim.go b/cmd/cue/cmd/trim.go
index abbda95..b0d22bb 100644
--- a/cmd/cue/cmd/trim.go
+++ b/cmd/cue/cmd/trim.go
@@ -515,9 +515,9 @@
 
 	for _, d := range decls {
 		if f, ok := d.(*ast.Field); ok {
-			label, _ := internal.LabelName(f.Label)
+			label, _, err := ast.LabelName(f.Label)
 			v := m.Lookup(label)
-			if inNodes(rm, f.Value) && (allow || v.Exists()) {
+			if err == nil && inNodes(rm, f.Value) && (allow || v.Exists()) {
 				continue
 			}
 		}
diff --git a/cue/ast/ast.go b/cue/ast/ast.go
index fb93b3f..5d0dce4 100644
--- a/cue/ast/ast.go
+++ b/cue/ast/ast.go
@@ -127,7 +127,7 @@
 func (*Package) declNode()      {}
 func (*CommentGroup) declNode() {}
 
-// A Label is any prduction that can be used as a LHS label.
+// A Label is any production that can be used as a LHS label.
 type Label interface {
 	Node
 	labelNode()
diff --git a/cue/ast/astutil/resolve.go b/cue/ast/astutil/resolve.go
index 2ebfe2f..d080688 100644
--- a/cue/ast/astutil/resolve.go
+++ b/cue/ast/astutil/resolve.go
@@ -66,11 +66,16 @@
 	for _, d := range decls {
 		switch x := d.(type) {
 		case *ast.Field:
-			name, _ := internal.LabelName(x.Label)
-			s.insert(name, x.Value)
+			// TODO: switch to ast's implementation
+			name, isIdent := internal.LabelName(x.Label)
+			if isIdent {
+				s.insert(name, x.Value)
+			}
 		case *ast.Alias:
-			name, _ := internal.LabelName(x.Ident)
-			s.insert(name, x)
+			name, isIdent, _ := ast.LabelName(x.Ident)
+			if isIdent {
+				s.insert(name, x)
+			}
 			// Handle imports
 		}
 	}
@@ -149,8 +154,10 @@
 			walk(s, label)
 		case *ast.TemplateLabel:
 			s := newScope(s.file, s, x, nil)
-			name, _ := internal.LabelName(label)
-			s.insert(name, x.Label) // Field used for entire lambda.
+			name, err := ast.ParseIdent(label.Ident)
+			if err == nil {
+				s.insert(name, x.Label) // Field used for entire lambda.
+			}
 			walk(s, x.Value)
 			return nil
 		}
@@ -180,7 +187,7 @@
 		return nil
 
 	case *ast.Ident:
-		name, ok := internal.LabelName(x)
+		name, ok, _ := ast.LabelName(x)
 		if !ok {
 			break
 		}
@@ -214,11 +221,15 @@
 			walk(s, f.Source)
 			s = newScope(s.file, s, f, nil)
 			if f.Key != nil {
-				name, _ := internal.LabelName(f.Key)
-				s.insert(name, f.Key)
+				name, err := ast.ParseIdent(f.Key)
+				if err == nil {
+					s.insert(name, f.Key)
+				}
 			}
-			name, _ := internal.LabelName(f.Value)
-			s.insert(name, f.Value)
+			name, err := ast.ParseIdent(f.Value)
+			if err == nil {
+				s.insert(name, f.Value)
+			}
 		} else {
 			walk(s, c)
 		}
diff --git a/cue/ast/ident.go b/cue/ast/ident.go
index f153b51..58e7811 100644
--- a/cue/ast/ident.go
+++ b/cue/ast/ident.go
@@ -93,3 +93,53 @@
 
 	return ident, nil
 }
+
+// LabelName reports the name of a label, whether it is an identifier
+// (it binds a value to a scope), and whether it is valid.
+// Keywords that are allowed in label positions are interpreted accordingly.
+//
+// Examples:
+//
+//     Label   Result
+//     foo     "foo"  true   nil
+//     `a-b`   "a-b"  true   nil
+//     true    true   true   nil
+//     "foo"   "foo"  false  nil
+//     `a-b    ""     false  invalid identifier
+//     "foo    ""     false  invalid string
+//     "\(x)"  ""     false  errors.Is(err, ErrIsExpression)
+//     <A>     "A"    false  errors.Is(err, ErrIsExpression)
+//
+func LabelName(l Label) (name string, isIdent bool, err error) {
+	switch n := l.(type) {
+	case *Ident:
+		str, err := ParseIdent(n)
+		if err != nil {
+			return "", false, err
+		}
+		return str, true, nil
+
+	case *BasicLit:
+		switch n.Kind {
+		case token.STRING:
+			// Use strconv to only allow double-quoted, single-line strings.
+			str, err := strconv.Unquote(n.Value)
+			if err != nil {
+				err = errors.Newf(l.Pos(), "invalid")
+			}
+			return str, false, err
+
+		case token.NULL, token.TRUE, token.FALSE:
+			return n.Value, true, nil
+
+			// TODO: allow numbers to be fields?
+		}
+	}
+	// This includes interpolation and template labels.
+	return "", false, errors.Wrapf(ErrIsExpression, l.Pos(),
+		"label is interpolation or template")
+}
+
+// ErrIsExpression reports whether a label is an expression.
+// This error is never returned directly. Use errors.Is or xerrors.Is.
+var ErrIsExpression = errors.New("not a concrete label")
diff --git a/cue/ast/ident_test.go b/cue/ast/ident_test.go
new file mode 100644
index 0000000..a32f451
--- /dev/null
+++ b/cue/ast/ident_test.go
@@ -0,0 +1,86 @@
+// 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 ast_test
+
+import (
+	"testing"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/format"
+	"cuelang.org/go/cue/token"
+	"github.com/stretchr/testify/assert"
+	"golang.org/x/xerrors"
+)
+
+func TestLabelName(t *testing.T) {
+	testCases := []struct {
+		in      ast.Label
+		out     string
+		isIdent bool
+		err     bool
+		expr    bool
+	}{{
+		in:      ast.NewString("foo-bar"),
+		out:     "foo-bar",
+		isIdent: false,
+	}, {
+		in:      ast.NewString("foo bar"),
+		out:     "foo bar",
+		isIdent: false,
+	}, {
+		in:      &ast.Ident{Name: "`foo`"},
+		out:     "foo",
+		isIdent: true,
+	}, {
+		in:      &ast.Ident{Name: "`foo-bar`"},
+		out:     "foo-bar",
+		isIdent: true,
+	}, {
+		in:      &ast.Ident{Name: "`foo-bar\x00`"},
+		out:     "",
+		isIdent: false,
+		err:     true,
+	}, {
+		in:      &ast.Ident{Name: "`foo-bar\x00`"},
+		out:     "",
+		isIdent: false,
+		err:     true,
+	}, {
+		in:      &ast.BasicLit{Kind: token.TRUE, Value: "true"},
+		out:     "true",
+		isIdent: true,
+	}, {
+		in:      &ast.BasicLit{Kind: token.STRING, Value: `"foo`},
+		out:     "",
+		isIdent: false,
+		err:     true,
+	}, {
+		in:      &ast.Interpolation{Elts: []ast.Expr{ast.NewString("foo")}},
+		out:     "",
+		isIdent: false,
+		err:     true,
+		expr:    true,
+	}}
+	for _, tc := range testCases {
+		b, _ := format.Node(tc.in)
+		t.Run(string(b), func(t *testing.T) {
+			str, isIdent, err := ast.LabelName(tc.in)
+			assert.Equal(t, tc.out, str, "value")
+			assert.Equal(t, tc.isIdent, isIdent, "isIdent")
+			assert.Equal(t, tc.err, err != nil, "err")
+			assert.Equal(t, tc.expr, xerrors.Is(err, ast.ErrIsExpression), "expr")
+		})
+	}
+}
diff --git a/cue/build.go b/cue/build.go
index cff3d7a..532cdde 100644
--- a/cue/build.go
+++ b/cue/build.go
@@ -267,8 +267,8 @@
 func (idx *index) nodeLabel(n ast.Node) (f label, ok bool) {
 	switch x := n.(type) {
 	case *ast.BasicLit:
-		name, ok := internal.LabelName(x)
-		return idx.label(name, false), ok
+		name, _, err := ast.LabelName(x)
+		return idx.label(name, false), err == nil
 	case *ast.Ident:
 		name, err := ast.ParseIdent(x)
 		return idx.label(name, true), err == nil
diff --git a/internal/third_party/yaml/decode.go b/internal/third_party/yaml/decode.go
index 749374e..40ddfed 100644
--- a/internal/third_party/yaml/decode.go
+++ b/internal/third_party/yaml/decode.go
@@ -16,7 +16,6 @@
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/token"
-	"cuelang.org/go/internal"
 )
 
 const (
@@ -697,8 +696,8 @@
 			key := labelStr(label)
 			for _, decl := range m.Elts {
 				f := decl.(*ast.Field)
-				name, _ := internal.LabelName(f.Label)
-				if name == key {
+				name, _, err := ast.LabelName(f.Label)
+				if err == nil && name == key {
 					f.Value = d.unmarshal(n.children[i+1])
 					continue outer
 				}