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 }