cuelang.org/go@v0.10.1/cue/load/tags.go (about)

     1  // Copyright 2020 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 load
    16  
    17  import (
    18  	"crypto/rand"
    19  	"encoding/hex"
    20  	"os"
    21  	"os/user"
    22  	"runtime"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"cuelang.org/go/cue"
    28  	"cuelang.org/go/cue/ast"
    29  	"cuelang.org/go/cue/build"
    30  	"cuelang.org/go/cue/errors"
    31  	"cuelang.org/go/cue/token"
    32  	"cuelang.org/go/internal"
    33  	"cuelang.org/go/internal/buildattr"
    34  	"cuelang.org/go/internal/cli"
    35  )
    36  
    37  type tagger struct {
    38  	cfg *Config
    39  	// tagMap holds true for all the tags in cfg.Tags that
    40  	// are not associated with a value.
    41  	tagMap map[string]bool
    42  	// tags keeps a record of all the @tag attibutes found in files.
    43  	tags         []*tag // tags found in files
    44  	replacements map[ast.Node]ast.Node
    45  
    46  	// mu guards the usedTags map.
    47  	mu sync.Mutex
    48  	// usedTags keeps a record of all the tag attributes found in files.
    49  	usedTags map[string]bool
    50  }
    51  
    52  func newTagger(c *Config) *tagger {
    53  	tagMap := map[string]bool{}
    54  	for _, t := range c.Tags {
    55  		if !strings.ContainsRune(t, '=') {
    56  			tagMap[t] = true
    57  		}
    58  	}
    59  	return &tagger{
    60  		cfg:      c,
    61  		tagMap:   tagMap,
    62  		usedTags: make(map[string]bool),
    63  	}
    64  }
    65  
    66  // tagIsSet reports whether the tag with the given key
    67  // is enabled. It also updates t.usedTags to
    68  // reflect that the tag has been seen.
    69  func (tg *tagger) tagIsSet(key string) bool {
    70  	tg.mu.Lock()
    71  	tg.usedTags[key] = true
    72  	tg.mu.Unlock()
    73  	return tg.tagMap[key]
    74  }
    75  
    76  // A TagVar represents an injection variable.
    77  type TagVar struct {
    78  	// Func returns an ast for a tag variable. It is only called once
    79  	// per evaluation of a configuration.
    80  	Func func() (ast.Expr, error)
    81  
    82  	// Description documents this TagVar.
    83  	Description string
    84  }
    85  
    86  const rfc3339 = "2006-01-02T15:04:05.999999999Z"
    87  
    88  // DefaultTagVars creates a new map with a set of supported injection variables.
    89  func DefaultTagVars() map[string]TagVar {
    90  	return map[string]TagVar{
    91  		"now": {
    92  			Func: func() (ast.Expr, error) {
    93  				return ast.NewString(time.Now().UTC().Format(rfc3339)), nil
    94  			},
    95  		},
    96  		"os": {
    97  			Func: func() (ast.Expr, error) {
    98  				return ast.NewString(runtime.GOOS), nil
    99  			},
   100  		},
   101  		"arch": {
   102  			Func: func() (ast.Expr, error) {
   103  				return ast.NewString(runtime.GOARCH), nil
   104  			},
   105  		},
   106  		"cwd": {
   107  			Func: func() (ast.Expr, error) {
   108  				return varToString(os.Getwd())
   109  			},
   110  		},
   111  		"username": {
   112  			Func: func() (ast.Expr, error) {
   113  				u, err := user.Current()
   114  				if err != nil {
   115  					return nil, err
   116  				}
   117  				return ast.NewString(u.Username), nil
   118  			},
   119  		},
   120  		"hostname": {
   121  			Func: func() (ast.Expr, error) {
   122  				return varToString(os.Hostname())
   123  			},
   124  		},
   125  		"rand": {
   126  			Func: func() (ast.Expr, error) {
   127  				var b [16]byte
   128  				_, err := rand.Read(b[:])
   129  				if err != nil {
   130  					return nil, err
   131  				}
   132  				var hx [34]byte
   133  				hx[0] = '0'
   134  				hx[1] = 'x'
   135  				hex.Encode(hx[2:], b[:])
   136  				return ast.NewLit(token.INT, string(hx[:])), nil
   137  			},
   138  		},
   139  	}
   140  }
   141  
   142  func varToString(s string, err error) (ast.Expr, error) {
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  	x := ast.NewString(s)
   147  	return x, nil
   148  }
   149  
   150  // A tag binds an identifier to a field to allow passing command-line values.
   151  //
   152  // A tag is of the form
   153  //
   154  //	@tag(<name>,[type=(string|int|number|bool)][,short=<shorthand>+])
   155  //
   156  // The name is mandatory and type defaults to string. Tags are set using the -t
   157  // option on the command line. -t name=value will parse value for the type
   158  // defined for name and set the field for which this tag was defined to this
   159  // value. A tag may be associated with multiple fields.
   160  //
   161  // Tags also allow shorthands. If a shorthand bar is declared for a tag with
   162  // name foo, then -t bar is identical to -t foo=bar.
   163  //
   164  // It is a deliberate choice to not allow other values to be associated with
   165  // shorthands than the shorthand name itself. Doing so would create a powerful
   166  // mechanism that would assign different values to different fields based on the
   167  // same shorthand, duplicating functionality that is already available in CUE.
   168  type tag struct {
   169  	key            string
   170  	kind           cue.Kind
   171  	shorthands     []string
   172  	vars           string // -T flag
   173  	hasReplacement bool
   174  
   175  	field *ast.Field
   176  }
   177  
   178  func parseTag(pos token.Pos, body string) (t *tag, err errors.Error) {
   179  	t = &tag{}
   180  	t.kind = cue.StringKind
   181  
   182  	a := internal.ParseAttrBody(pos, body)
   183  
   184  	t.key, _ = a.String(0)
   185  	if !ast.IsValidIdent(t.key) {
   186  		return t, errors.Newf(pos, "invalid identifier %q", t.key)
   187  	}
   188  
   189  	if s, ok, _ := a.Lookup(1, "type"); ok {
   190  		switch s {
   191  		case "string":
   192  		case "int":
   193  			t.kind = cue.IntKind
   194  		case "number":
   195  			t.kind = cue.NumberKind
   196  		case "bool":
   197  			t.kind = cue.BoolKind
   198  		default:
   199  			return t, errors.Newf(pos, "invalid type %q", s)
   200  		}
   201  	}
   202  
   203  	if s, ok, _ := a.Lookup(1, "short"); ok {
   204  		for _, s := range strings.Split(s, "|") {
   205  			if !ast.IsValidIdent(t.key) {
   206  				return t, errors.Newf(pos, "invalid identifier %q", s)
   207  			}
   208  			t.shorthands = append(t.shorthands, s)
   209  		}
   210  	}
   211  
   212  	if s, ok, _ := a.Lookup(1, "var"); ok {
   213  		t.vars = s
   214  	}
   215  
   216  	return t, nil
   217  }
   218  
   219  func (t *tag) inject(value string, tg *tagger) errors.Error {
   220  	e, err := cli.ParseValue(token.NoPos, t.key, value, t.kind)
   221  	t.injectValue(e, tg)
   222  	return err
   223  }
   224  
   225  func (t *tag) injectValue(x ast.Expr, tg *tagger) {
   226  	injected := ast.NewBinExpr(token.AND, t.field.Value, x)
   227  	if tg.replacements == nil {
   228  		tg.replacements = make(map[ast.Node]ast.Node)
   229  	}
   230  	tg.replacements[t.field.Value] = injected
   231  	t.field.Value = injected
   232  	t.hasReplacement = true
   233  }
   234  
   235  // findTags defines which fields may be associated with tags.
   236  //
   237  // TODO: should we limit the depth at which tags may occur?
   238  func findTags(b *build.Instance) (tags []*tag, errs errors.Error) {
   239  	findInvalidTags := func(x ast.Node, msg string) {
   240  		ast.Walk(x, nil, func(n ast.Node) {
   241  			if f, ok := n.(*ast.Field); ok {
   242  				for _, a := range f.Attrs {
   243  					if key, _ := a.Split(); key == "tag" {
   244  						errs = errors.Append(errs, errors.Newf(a.Pos(), msg))
   245  						// TODO: add position of x.
   246  					}
   247  				}
   248  			}
   249  		})
   250  	}
   251  	for _, f := range b.Files {
   252  		ast.Walk(f, func(n ast.Node) bool {
   253  			switch x := n.(type) {
   254  			case *ast.ListLit:
   255  				findInvalidTags(n, "@tag not allowed within lists")
   256  				return false
   257  
   258  			case *ast.Comprehension:
   259  				findInvalidTags(n, "@tag not allowed within comprehension")
   260  				return false
   261  
   262  			case *ast.Field:
   263  				// TODO: allow optional fields?
   264  				_, _, err := ast.LabelName(x.Label)
   265  				_, ok := internal.ConstraintToken(x)
   266  				if err != nil || ok {
   267  					findInvalidTags(n, "@tag not allowed within field constraint")
   268  					return false
   269  				}
   270  
   271  				for _, a := range x.Attrs {
   272  					key, body := a.Split()
   273  					if key != "tag" {
   274  						continue
   275  					}
   276  					t, err := parseTag(a.Pos(), body)
   277  					if err != nil {
   278  						errs = errors.Append(errs, err)
   279  						continue
   280  					}
   281  					t.field = x
   282  					tags = append(tags, t)
   283  				}
   284  			}
   285  			return true
   286  		}, nil)
   287  	}
   288  	return tags, errs
   289  }
   290  
   291  func (tg *tagger) injectTags(tags []string) errors.Error {
   292  	// Parses command line args
   293  	for _, s := range tags {
   294  		p := strings.Index(s, "=")
   295  		found := tg.usedTags[s]
   296  		if p > 0 { // key-value
   297  			for _, t := range tg.tags {
   298  				if t.key == s[:p] {
   299  					found = true
   300  					if err := t.inject(s[p+1:], tg); err != nil {
   301  						return err
   302  					}
   303  				}
   304  			}
   305  			if !found {
   306  				return errors.Newf(token.NoPos, "no tag for %q", s[:p])
   307  			}
   308  		} else { // shorthand
   309  			for _, t := range tg.tags {
   310  				for _, sh := range t.shorthands {
   311  					if sh == s {
   312  						found = true
   313  						if err := t.inject(s, tg); err != nil {
   314  							return err
   315  						}
   316  					}
   317  				}
   318  			}
   319  			if !found {
   320  				return errors.Newf(token.NoPos, "tag %q not used in any file", s)
   321  			}
   322  		}
   323  	}
   324  
   325  	if tg.cfg.TagVars != nil {
   326  		vars := map[string]ast.Expr{}
   327  
   328  		// Inject tag variables if the tag wasn't already set.
   329  		for _, t := range tg.tags {
   330  			if t.hasReplacement || t.vars == "" {
   331  				continue
   332  			}
   333  			x, ok := vars[t.vars]
   334  			if !ok {
   335  				tv, ok := tg.cfg.TagVars[t.vars]
   336  				if !ok {
   337  					return errors.Newf(token.NoPos,
   338  						"tag variable '%s' not found", t.vars)
   339  				}
   340  				tag, err := tv.Func()
   341  				if err != nil {
   342  					return errors.Wrapf(err, token.NoPos,
   343  						"error getting tag variable '%s'", t.vars)
   344  				}
   345  				x = tag
   346  				vars[t.vars] = tag
   347  			}
   348  			if x != nil {
   349  				t.injectValue(x, tg)
   350  			}
   351  		}
   352  	}
   353  	return nil
   354  }
   355  
   356  func shouldBuildFile(f *ast.File, tagIsSet func(key string) bool) errors.Error {
   357  	ok, attr, err := buildattr.ShouldBuildFile(f, tagIsSet)
   358  	if err != nil {
   359  		return err
   360  	}
   361  	if ok {
   362  		return nil
   363  	}
   364  	if key, body := attr.Split(); key == "if" {
   365  		return excludeError{errors.Newf(attr.Pos(), "@if(%s) did not match", body)}
   366  	} else {
   367  		return excludeError{errors.Newf(attr.Pos(), "@ignore() attribute found")}
   368  	}
   369  }