github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/cue/ast/ident.go (about) 1 // Copyright 2019 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ast 16 17 import ( 18 "strconv" 19 "strings" 20 "unicode" 21 "unicode/utf8" 22 23 "github.com/joomcode/cue/cue/errors" 24 "github.com/joomcode/cue/cue/token" 25 ) 26 27 func isLetter(ch rune) bool { 28 return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch >= utf8.RuneSelf && unicode.IsLetter(ch) 29 } 30 31 func isDigit(ch rune) bool { 32 // TODO(mpvl): Is this correct? 33 return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch) 34 } 35 36 // IsValidIdent reports whether str is a valid identifier. 37 func IsValidIdent(ident string) bool { 38 if ident == "" { 39 return false 40 } 41 42 // TODO: use consumed again to allow #0. 43 // consumed := false 44 if strings.HasPrefix(ident, "_") { 45 ident = ident[1:] 46 // consumed = true 47 if len(ident) == 0 { 48 return true 49 } 50 } 51 if strings.HasPrefix(ident, "#") { 52 ident = ident[1:] 53 // consumed = true 54 } 55 56 // if !consumed { 57 if r, _ := utf8.DecodeRuneInString(ident); isDigit(r) { 58 return false 59 } 60 // } 61 62 for _, r := range ident { 63 if isLetter(r) || isDigit(r) || r == '_' || r == '$' { 64 continue 65 } 66 return false 67 } 68 return true 69 } 70 71 // ParseIdent unquotes a possibly quoted identifier and validates 72 // if the result is valid. 73 // 74 // Deprecated: quoted identifiers are deprecated. Use aliases. 75 func ParseIdent(n *Ident) (string, error) { 76 return parseIdent(n.NamePos, n.Name) 77 } 78 79 func parseIdent(pos token.Pos, ident string) (string, error) { 80 if ident == "" { 81 return "", errors.Newf(pos, "empty identifier") 82 } 83 quoted := false 84 if ident[0] == '`' { 85 u, err := strconv.Unquote(ident) 86 if err != nil { 87 return "", errors.Newf(pos, "invalid quoted identifier") 88 } 89 ident = u 90 quoted = true 91 } 92 93 p := 0 94 if strings.HasPrefix(ident, "_") { 95 p++ 96 if len(ident) == 1 { 97 return ident, nil 98 } 99 } 100 if strings.HasPrefix(ident[p:], "#") { 101 p++ 102 // if len(ident) == p { 103 // return "", errors.Newf(pos, "invalid identifier '_#'") 104 // } 105 } 106 107 if p == 0 || ident[p-1] == '#' { 108 if r, _ := utf8.DecodeRuneInString(ident[p:]); isDigit(r) { 109 return "", errors.Newf(pos, "invalid character '%s' in identifier", string(r)) 110 } 111 } 112 113 for _, r := range ident[p:] { 114 if isLetter(r) || isDigit(r) || r == '_' || r == '$' { 115 continue 116 } 117 if r == '-' && quoted { 118 continue 119 } 120 return "", errors.Newf(pos, "invalid character '%s' in identifier", string(r)) 121 } 122 123 return ident, nil 124 } 125 126 // LabelName reports the name of a label, whether it is an identifier 127 // (it binds a value to a scope), and whether it is valid. 128 // Keywords that are allowed in label positions are interpreted accordingly. 129 // 130 // Examples: 131 // 132 // Label Result 133 // foo "foo" true nil 134 // true "true" true nil 135 // "foo" "foo" false nil 136 // "x-y" "x-y" false nil 137 // "foo "" false invalid string 138 // "\(x)" "" false errors.Is(err, ErrIsExpression) 139 // X=foo "foo" true nil 140 // 141 func LabelName(l Label) (name string, isIdent bool, err error) { 142 if a, ok := l.(*Alias); ok { 143 l, _ = a.Expr.(Label) 144 } 145 switch n := l.(type) { 146 case *ListLit: 147 // An expression, but not one that can evaluated. 148 return "", false, errors.Newf(l.Pos(), 149 "cannot reference fields with square brackets labels outside the field value") 150 151 case *Ident: 152 // TODO(legacy): use name = n.Name 153 name, err = ParseIdent(n) 154 if err != nil { 155 return "", false, err 156 } 157 isIdent = true 158 // TODO(legacy): remove this return once quoted identifiers are removed. 159 return name, isIdent, err 160 161 case *BasicLit: 162 switch n.Kind { 163 case token.STRING: 164 // Use strconv to only allow double-quoted, single-line strings. 165 name, err = strconv.Unquote(n.Value) 166 if err != nil { 167 err = errors.Newf(l.Pos(), "invalid") 168 } 169 170 case token.NULL, token.TRUE, token.FALSE: 171 name = n.Value 172 isIdent = true 173 174 default: 175 // TODO: allow numbers to be fields 176 // This includes interpolation and template labels. 177 return "", false, errors.Wrapf(ErrIsExpression, l.Pos(), 178 "cannot use numbers as fields") 179 } 180 181 default: 182 // This includes interpolation and template labels. 183 return "", false, errors.Wrapf(ErrIsExpression, l.Pos(), 184 "label is an expression") 185 } 186 if !IsValidIdent(name) { 187 isIdent = false 188 } 189 return name, isIdent, err 190 191 } 192 193 // ErrIsExpression reports whether a label is an expression. 194 // This error is never returned directly. Use errors.Is. 195 var ErrIsExpression = errors.New("not a concrete label")