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