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")