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 }