cuelang.org/go@v0.10.1/internal/encoding/yaml/decode.go (about) 1 package yaml 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "errors" 7 "fmt" 8 "io" 9 "regexp" 10 "strconv" 11 "strings" 12 "sync" 13 14 "gopkg.in/yaml.v3" 15 16 "cuelang.org/go/cue/ast" 17 "cuelang.org/go/cue/literal" 18 "cuelang.org/go/cue/token" 19 "cuelang.org/go/internal" 20 "cuelang.org/go/internal/cueexperiment" 21 tpyaml "cuelang.org/go/internal/third_party/yaml" 22 ) 23 24 // TODO(mvdan): we should sanity check that the decoder always produces valid CUE, 25 // as it is possible to construct a cue/ast syntax tree with invalid literals 26 // or with expressions that will always error, such as `float & 123`. 27 // 28 // One option would be to do this as part of the tests; a more general approach 29 // may be fuzzing, which would find more bugs and work for any decoder, 30 // although it may be slow as we need to involve the evaluator. 31 32 // Decoder is a temporary interface compatible with both the old and new yaml decoders. 33 type Decoder interface { 34 // Decode consumes a YAML value and returns it in CUE syntax tree node. 35 Decode() (ast.Expr, error) 36 } 37 38 // NewDecoder is a temporary constructor compatible with both the old and new yaml decoders. 39 // Note that the signature matches the new yaml decoder, as the old signature can only error 40 // when reading a source that isn't []byte. 41 func NewDecoder(filename string, b []byte) Decoder { 42 if cueexperiment.Flags.YAMLV3Decoder { 43 return newDecoder(filename, b) 44 } 45 dec, err := tpyaml.NewDecoder(filename, b) 46 if err != nil { 47 panic(err) // should never happen as we give it []byte 48 } 49 return dec 50 } 51 52 // decoder wraps a [yaml.Decoder] to extract CUE syntax tree nodes. 53 type decoder struct { 54 yamlDecoder yaml.Decoder 55 56 // yamlNonEmpty is true once yamlDecoder tells us the input YAML wasn't empty. 57 // Useful so that we can extract "null" when the input is empty. 58 yamlNonEmpty bool 59 60 // decodeErr is returned by any further calls to Decode when not nil. 61 decodeErr error 62 63 tokFile *token.File 64 tokLines []int 65 66 // pendingHeadComments collects the head (preceding) comments 67 // from the YAML nodes we are extracting. 68 // We can't add comments to a CUE syntax tree node until we've created it, 69 // but we need to extract these comments first since they have earlier positions. 70 pendingHeadComments []*ast.Comment 71 72 // extractingAliases ensures we don't loop forever when expanding YAML anchors. 73 extractingAliases map[*yaml.Node]bool 74 75 // lastPos is the last YAML node position that we decoded, 76 // used for working out relative positions such as token.NewSection. 77 // This position can only increase, moving forward in the file. 78 lastPos token.Position 79 80 // forceNewline ensures that the next position will be on a new line. 81 forceNewline bool 82 } 83 84 // TODO(mvdan): this can be io.Reader really, except that token.Pos is offset-based, 85 // so the only way to really have true Offset+Line+Col numbers is to know 86 // the size of the entire YAML node upfront. 87 // With json we can use RawMessage to know the size of the input 88 // before we extract into ast.Expr, but unfortunately, yaml.Node has no size. 89 90 // newDecoder creates a decoder for YAML values to extract CUE syntax tree nodes. 91 // 92 // The filename is used for position information in CUE syntax tree nodes 93 // as well as any errors encountered while decoding YAML. 94 func newDecoder(filename string, b []byte) *decoder { 95 // Note that yaml.v3 can insert a null node just past the end of the input 96 // in some edge cases, so we pretend that there's an extra newline 97 // so that we don't panic when handling such a position. 98 tokFile := token.NewFile(filename, 0, len(b)+1) 99 tokFile.SetLinesForContent(b) 100 return &decoder{ 101 tokFile: tokFile, 102 tokLines: append(tokFile.Lines(), len(b)), 103 yamlDecoder: *yaml.NewDecoder(bytes.NewReader(b)), 104 } 105 } 106 107 // Decode consumes a YAML value and returns it in CUE syntax tree node. 108 // 109 // A nil node with an io.EOF error is returned once no more YAML values 110 // are available for decoding. 111 func (d *decoder) Decode() (ast.Expr, error) { 112 if err := d.decodeErr; err != nil { 113 return nil, err 114 } 115 var yn yaml.Node 116 if err := d.yamlDecoder.Decode(&yn); err != nil { 117 if err == io.EOF { 118 // Any further Decode calls must return EOF to avoid an endless loop. 119 d.decodeErr = io.EOF 120 121 // If the input is empty, we produce a single null literal with EOF. 122 // Note that when the input contains "---", we get an empty document 123 // with a null scalar value inside instead. 124 if !d.yamlNonEmpty { 125 return &ast.BasicLit{ 126 Kind: token.NULL, 127 Value: "null", 128 }, nil 129 } 130 // If the input wasn't empty, we already decoded some CUE syntax nodes, 131 // so here we should just return io.EOF to stop. 132 return nil, io.EOF 133 } 134 // Unfortunately, yaml.v3's syntax errors are opaque strings, 135 // and they only include line numbers in some but not all cases. 136 // TODO(mvdan): improve upstream's errors so they are structured 137 // and always contain some position information. 138 e := err.Error() 139 if s, ok := strings.CutPrefix(e, "yaml: line "); ok { 140 // From "yaml: line 3: some issue" to "foo.yaml:3: some issue". 141 e = d.tokFile.Name() + ":" + s 142 } else if s, ok := strings.CutPrefix(e, "yaml:"); ok { 143 // From "yaml: some issue" to "foo.yaml: some issue". 144 e = d.tokFile.Name() + ":" + s 145 } else { 146 return nil, err 147 } 148 err = errors.New(e) 149 // Any further Decode calls repeat this error. 150 d.decodeErr = err 151 return nil, err 152 } 153 d.yamlNonEmpty = true 154 return d.extract(&yn) 155 } 156 157 // Unmarshal parses a single YAML value to a CUE expression. 158 func Unmarshal(filename string, data []byte) (ast.Expr, error) { 159 d := NewDecoder(filename, data) 160 n, err := d.Decode() 161 if err != nil { 162 if err == io.EOF { 163 return nil, nil // empty input 164 } 165 return nil, err 166 } 167 // TODO(mvdan): decoding the entire next value is unnecessary; 168 // consider either a "More" or "Done" method to tell if we are at EOF, 169 // or splitting the Decode method into two variants. 170 // This should use proper error values with positions as well. 171 if n2, err := d.Decode(); err == nil { 172 return nil, fmt.Errorf("%s: expected a single YAML document", n2.Pos()) 173 } else if err != io.EOF { 174 return nil, fmt.Errorf("expected a single YAML document: %v", err) 175 } 176 return n, nil 177 } 178 179 func (d *decoder) extract(yn *yaml.Node) (ast.Expr, error) { 180 d.addHeadCommentsToPending(yn) 181 var expr ast.Expr 182 var err error 183 switch yn.Kind { 184 case yaml.DocumentNode: 185 expr, err = d.document(yn) 186 case yaml.SequenceNode: 187 expr, err = d.sequence(yn) 188 case yaml.MappingNode: 189 expr, err = d.mapping(yn) 190 case yaml.ScalarNode: 191 expr, err = d.scalar(yn) 192 case yaml.AliasNode: 193 expr, err = d.alias(yn) 194 default: 195 return nil, d.posErrorf(yn, "unknown yaml node kind: %d", yn.Kind) 196 } 197 if err != nil { 198 return nil, err 199 } 200 d.addCommentsToNode(expr, yn, 1) 201 return expr, nil 202 } 203 204 // comments parses a newline-delimited list of YAML "#" comments 205 // and turns them into a list of cue/ast comments. 206 func (d *decoder) comments(src string) []*ast.Comment { 207 if src == "" { 208 return nil 209 } 210 var comments []*ast.Comment 211 for _, line := range strings.Split(src, "\n") { 212 if line == "" { 213 continue // yaml.v3 comments have a trailing newline at times 214 } 215 comments = append(comments, &ast.Comment{ 216 // Trim the leading "#". 217 // Note that yaml.v3 does not give us comment positions. 218 Text: "//" + line[1:], 219 }) 220 } 221 return comments 222 } 223 224 // addHeadCommentsToPending parses a node's head comments and adds them to a pending list, 225 // to be used later by addComments once a cue/ast node is constructed. 226 func (d *decoder) addHeadCommentsToPending(yn *yaml.Node) { 227 comments := d.comments(yn.HeadComment) 228 // TODO(mvdan): once yaml.v3 records comment positions, 229 // we can better ensure that sections separated by empty lines are kept that way. 230 // For now, all we can do is approximate by counting lines, 231 // and assuming that head comments are not separated from their node. 232 // This will be wrong in some cases, moving empty lines, but is better than nothing. 233 if len(d.pendingHeadComments) == 0 && len(comments) > 0 { 234 c := comments[0] 235 if d.lastPos.IsValid() && (yn.Line-len(comments))-d.lastPos.Line >= 2 { 236 c.Slash = c.Slash.WithRel(token.NewSection) 237 } 238 } 239 d.pendingHeadComments = append(d.pendingHeadComments, comments...) 240 } 241 242 // addCommentsToNode adds any pending head comments, plus a YAML node's line 243 // and foot comments, to a cue/ast node. 244 func (d *decoder) addCommentsToNode(n ast.Node, yn *yaml.Node, linePos int8) { 245 // cue/ast and cue/format are not able to attach a comment to a node 246 // when the comment immediately follows the node. 247 // For some nodes like fields, the best we can do is move the comments up. 248 // For the root-level struct, we do want to leave comments 249 // at the end of the document to be left at the very end. 250 // 251 // TODO(mvdan): can we do better? for example, support attaching trailing comments to a cue/ast.Node? 252 footComments := d.comments(yn.FootComment) 253 if _, ok := n.(*ast.StructLit); !ok { 254 d.pendingHeadComments = append(d.pendingHeadComments, footComments...) 255 footComments = nil 256 } 257 if comments := d.pendingHeadComments; len(comments) > 0 { 258 ast.AddComment(n, &ast.CommentGroup{ 259 Doc: true, 260 Position: 0, 261 List: comments, 262 }) 263 } 264 if comments := d.comments(yn.LineComment); len(comments) > 0 { 265 ast.AddComment(n, &ast.CommentGroup{ 266 Line: true, 267 Position: linePos, 268 List: comments, 269 }) 270 } 271 if comments := footComments; len(comments) > 0 { 272 ast.AddComment(n, &ast.CommentGroup{ 273 // After 100 tokens, so that the comment goes after the entire node. 274 // TODO(mvdan): this is hacky, can the cue/ast API support trailing comments better? 275 Position: 100, 276 List: comments, 277 }) 278 } 279 d.pendingHeadComments = nil 280 } 281 282 func (d *decoder) posErrorf(yn *yaml.Node, format string, args ...any) error { 283 // TODO(mvdan): use columns as well; for now they are left out to avoid test churn 284 // return fmt.Errorf(d.pos(n).String()+" "+format, args...) 285 return fmt.Errorf(d.tokFile.Name()+":"+strconv.Itoa(yn.Line)+": "+format, args...) 286 } 287 288 // pos converts a YAML node position to a cue/ast position. 289 // Note that this method uses and updates the last position in lastPos, 290 // so it should be called on YAML nodes in increasing position order. 291 func (d *decoder) pos(yn *yaml.Node) token.Pos { 292 // Calculate the position's offset via the line and column numbers. 293 offset := d.tokLines[yn.Line-1] + (yn.Column - 1) 294 pos := d.tokFile.Pos(offset, token.NoRelPos) 295 296 if d.forceNewline { 297 d.forceNewline = false 298 pos = pos.WithRel(token.Newline) 299 } else if d.lastPos.IsValid() { 300 switch { 301 case yn.Line-d.lastPos.Line >= 2: 302 pos = pos.WithRel(token.NewSection) 303 case yn.Line-d.lastPos.Line == 1: 304 pos = pos.WithRel(token.Newline) 305 case yn.Column-d.lastPos.Column > 0: 306 pos = pos.WithRel(token.Blank) 307 default: 308 pos = pos.WithRel(token.NoSpace) 309 } 310 // If for any reason the node's position is before the last position, 311 // give up and return an empty position. Akin to: yn.Pos().Before(d.lastPos) 312 // 313 // TODO(mvdan): Brought over from the old decoder; when does this happen? 314 // Can we get rid of those edge cases and this bit of logic? 315 if yn.Line < d.lastPos.Line || (yn.Line == d.lastPos.Line && yn.Column < d.lastPos.Column) { 316 return token.NoPos 317 } 318 } 319 d.lastPos = token.Position{Line: yn.Line, Column: yn.Column} 320 return pos 321 } 322 323 func (d *decoder) document(yn *yaml.Node) (ast.Expr, error) { 324 if n := len(yn.Content); n != 1 { 325 return nil, d.posErrorf(yn, "yaml document nodes are meant to have one content node but have %d", n) 326 } 327 return d.extract(yn.Content[0]) 328 } 329 330 func (d *decoder) sequence(yn *yaml.Node) (ast.Expr, error) { 331 list := &ast.ListLit{ 332 Lbrack: d.pos(yn).WithRel(token.Blank), 333 } 334 multiline := false 335 if len(yn.Content) > 0 { 336 multiline = yn.Line < yn.Content[len(yn.Content)-1].Line 337 } 338 339 // If a list is empty, or ends with a struct, the closing `]` is on the same line. 340 closeSameLine := true 341 for _, c := range yn.Content { 342 d.forceNewline = multiline 343 elem, err := d.extract(c) 344 if err != nil { 345 return nil, err 346 } 347 list.Elts = append(list.Elts, elem) 348 // A list of structs begins with `[{`, so let it end with `}]`. 349 _, closeSameLine = elem.(*ast.StructLit) 350 } 351 if multiline && !closeSameLine { 352 list.Rbrack = list.Rbrack.WithRel(token.Newline) 353 } 354 return list, nil 355 } 356 357 func (d *decoder) mapping(yn *yaml.Node) (ast.Expr, error) { 358 strct := &ast.StructLit{} 359 multiline := false 360 if len(yn.Content) > 0 { 361 multiline = yn.Line < yn.Content[len(yn.Content)-1].Line 362 } 363 364 if err := d.insertMap(yn, strct, multiline, false); err != nil { 365 return nil, err 366 } 367 // TODO(mvdan): moving these positions above insertMap breaks a few tests, why? 368 strct.Lbrace = d.pos(yn).WithRel(token.Blank) 369 if multiline { 370 strct.Rbrace = strct.Lbrace.WithRel(token.Newline) 371 } else { 372 strct.Rbrace = strct.Lbrace 373 } 374 return strct, nil 375 } 376 377 func (d *decoder) insertMap(yn *yaml.Node, m *ast.StructLit, multiline, mergeValues bool) error { 378 l := len(yn.Content) 379 outer: 380 for i := 0; i < l; i += 2 { 381 if multiline { 382 d.forceNewline = true 383 } 384 yk, yv := yn.Content[i], yn.Content[i+1] 385 d.addHeadCommentsToPending(yk) 386 if isMerge(yk) { 387 mergeValues = true 388 if err := d.merge(yv, m, multiline); err != nil { 389 return err 390 } 391 continue 392 } 393 if yk.Kind != yaml.ScalarNode { 394 return d.posErrorf(yn, "invalid map key: %v", yk.ShortTag()) 395 } 396 397 field := &ast.Field{} 398 label, err := d.label(yk) 399 if err != nil { 400 return err 401 } 402 d.addCommentsToNode(field, yk, 2) 403 field.Label = label 404 405 if mergeValues { 406 key := labelStr(label) 407 for _, decl := range m.Elts { 408 f := decl.(*ast.Field) 409 name, _, err := ast.LabelName(f.Label) 410 if err == nil && name == key { 411 f.Value, err = d.extract(yv) 412 if err != nil { 413 return err 414 } 415 continue outer 416 } 417 } 418 } 419 420 value, err := d.extract(yv) 421 if err != nil { 422 return err 423 } 424 field.Value = value 425 426 m.Elts = append(m.Elts, field) 427 } 428 return nil 429 } 430 431 func (d *decoder) merge(yn *yaml.Node, m *ast.StructLit, multiline bool) error { 432 switch yn.Kind { 433 case yaml.MappingNode: 434 return d.insertMap(yn, m, multiline, true) 435 case yaml.AliasNode: 436 return d.insertMap(yn.Alias, m, multiline, true) 437 case yaml.SequenceNode: 438 // Step backwards as earlier nodes take precedence. 439 for i := len(yn.Content) - 1; i >= 0; i-- { 440 if err := d.merge(yn.Content[i], m, multiline); err != nil { 441 return err 442 } 443 } 444 return nil 445 default: 446 return d.posErrorf(yn, "map merge requires map or sequence of maps as the value") 447 } 448 } 449 450 func (d *decoder) label(yn *yaml.Node) (ast.Label, error) { 451 pos := d.pos(yn) 452 453 expr, err := d.scalar(yn) 454 if err != nil { 455 return nil, err 456 } 457 switch expr := expr.(type) { 458 case *ast.BasicLit: 459 if expr.Kind == token.STRING { 460 if ast.IsValidIdent(yn.Value) && !internal.IsDefOrHidden(yn.Value) { 461 return &ast.Ident{ 462 NamePos: pos, 463 Name: yn.Value, 464 }, nil 465 } 466 ast.SetPos(expr, pos) 467 return expr, nil 468 } 469 470 return &ast.BasicLit{ 471 ValuePos: pos, 472 Kind: token.STRING, 473 Value: literal.Label.Quote(expr.Value), 474 }, nil 475 476 default: 477 return nil, d.posErrorf(yn, "invalid label "+yn.Value) 478 } 479 } 480 481 const ( 482 // TODO(mvdan): The strings below are from yaml.v3; should we be relying on upstream somehow? 483 nullTag = "!!null" 484 boolTag = "!!bool" 485 strTag = "!!str" 486 intTag = "!!int" 487 floatTag = "!!float" 488 timestampTag = "!!timestamp" 489 seqTag = "!!seq" 490 mapTag = "!!map" 491 binaryTag = "!!binary" 492 mergeTag = "!!merge" 493 ) 494 495 // rxAnyOctalYaml11 uses the implicit tag resolution regular expression for base-8 integers 496 // from YAML's 1.1 spec, but including the 8 and 9 digits which aren't valid for octal integers. 497 var rxAnyOctalYaml11 = sync.OnceValue(func() *regexp.Regexp { 498 return regexp.MustCompile(`^[-+]?0[0-9_]+$`) 499 }) 500 501 func (d *decoder) scalar(yn *yaml.Node) (ast.Expr, error) { 502 tag := yn.ShortTag() 503 // If the YAML scalar has no explicit tag, yaml.v3 infers a float tag, 504 // and the value looks like a YAML 1.1 octal literal, 505 // that means the input value was like `01289` and not a valid octal integer. 506 // The safest thing to do, and what most YAML decoders do, is to interpret as a string. 507 if yn.Style&yaml.TaggedStyle == 0 && tag == floatTag && rxAnyOctalYaml11().MatchString(yn.Value) { 508 tag = strTag 509 } 510 switch tag { 511 // TODO: use parse literal or parse expression instead. 512 case timestampTag: 513 return &ast.BasicLit{ 514 ValuePos: d.pos(yn), 515 Kind: token.STRING, 516 Value: literal.String.Quote(yn.Value), 517 }, nil 518 case strTag: 519 return &ast.BasicLit{ 520 ValuePos: d.pos(yn), 521 Kind: token.STRING, 522 Value: literal.String.WithOptionalTabIndent(1).Quote(yn.Value), 523 }, nil 524 525 case binaryTag: 526 data, err := base64.StdEncoding.DecodeString(yn.Value) 527 if err != nil { 528 return nil, d.posErrorf(yn, "!!binary value contains invalid base64 data") 529 } 530 return &ast.BasicLit{ 531 ValuePos: d.pos(yn), 532 Kind: token.STRING, 533 Value: literal.Bytes.Quote(string(data)), 534 }, nil 535 536 case boolTag: 537 t := false 538 switch yn.Value { 539 // TODO(mvdan): The strings below are from yaml.v3; should we be relying on upstream somehow? 540 case "true", "True", "TRUE": 541 t = true 542 } 543 lit := ast.NewBool(t) 544 lit.ValuePos = d.pos(yn) 545 return lit, nil 546 547 case intTag: 548 // Convert YAML octal to CUE octal. If YAML accepted an invalid 549 // integer, just convert it as well to ensure CUE will fail. 550 value := yn.Value 551 if len(value) > 1 && value[0] == '0' && value[1] <= '9' { 552 value = "0o" + value[1:] 553 } 554 var info literal.NumInfo 555 // We make the assumption that any valid YAML integer literal will be a valid 556 // CUE integer literal as well, with the only exception of octal numbers above. 557 // Note that `!!int 123.456` is not allowed. 558 if err := literal.ParseNum(value, &info); err != nil { 559 return nil, d.posErrorf(yn, "cannot decode %q as %s: %v", value, tag, err) 560 } else if !info.IsInt() { 561 return nil, d.posErrorf(yn, "cannot decode %q as %s: not a literal number", value, tag) 562 } 563 return d.makeNum(yn, value, token.INT), nil 564 565 case floatTag: 566 value := yn.Value 567 // TODO(mvdan): The strings below are from yaml.v3; should we be relying on upstream somehow? 568 switch value { 569 case ".inf", ".Inf", ".INF", "+.inf", "+.Inf", "+.INF": 570 value = "+Inf" 571 case "-.inf", "-.Inf", "-.INF": 572 value = "-Inf" 573 case ".nan", ".NaN", ".NAN": 574 value = "NaN" 575 default: 576 var info literal.NumInfo 577 // We make the assumption that any valid YAML float literal will be a valid 578 // CUE float literal as well, with the only exception of Inf/NaN above. 579 // Note that `!!float 123` is allowed. 580 if err := literal.ParseNum(value, &info); err != nil { 581 return nil, d.posErrorf(yn, "cannot decode %q as %s: %v", value, tag, err) 582 } 583 // If the decoded YAML scalar was explicitly or implicitly a float, 584 // and the scalar literal looks like an integer, 585 // unify it with "number" to record the fact that it was represented as a float. 586 // Don't unify with float, as `float & 123` is invalid, and there's no need 587 // to forbid representing the number as an integer either. 588 if yn.Tag != "" { 589 if p := strings.IndexAny(value, ".eEiInN"); p == -1 { 590 // TODO: number(v) when we have conversions 591 // TODO(mvdan): don't shove the unification inside a BasicLit.Value string 592 // 593 // TODO(mvdan): would it be better to do turn `!!float 123` into `123.0` 594 // rather than `number & 123`? Note that `float & 123` is an error. 595 value = fmt.Sprintf("number & %s", value) 596 } 597 } 598 } 599 return d.makeNum(yn, value, token.FLOAT), nil 600 601 case nullTag: 602 return &ast.BasicLit{ 603 ValuePos: d.pos(yn).WithRel(token.Blank), 604 Kind: token.NULL, 605 Value: "null", 606 }, nil 607 default: 608 return nil, d.posErrorf(yn, "cannot unmarshal tag %q", tag) 609 } 610 } 611 612 func (d *decoder) makeNum(yn *yaml.Node, val string, kind token.Token) (expr ast.Expr) { 613 val, negative := strings.CutPrefix(val, "-") 614 expr = &ast.BasicLit{ 615 ValuePos: d.pos(yn), 616 Kind: kind, 617 Value: val, 618 } 619 if negative { 620 expr = &ast.UnaryExpr{ 621 OpPos: d.pos(yn), 622 Op: token.SUB, 623 X: expr, 624 } 625 } 626 return expr 627 } 628 629 func (d *decoder) alias(yn *yaml.Node) (ast.Expr, error) { 630 if d.extractingAliases[yn] { 631 // TODO this could actually be allowed in some circumstances. 632 return nil, d.posErrorf(yn, "anchor %q value contains itself", yn.Value) 633 } 634 if d.extractingAliases == nil { 635 d.extractingAliases = make(map[*yaml.Node]bool) 636 } 637 d.extractingAliases[yn] = true 638 var node ast.Expr 639 node, err := d.extract(yn.Alias) 640 delete(d.extractingAliases, yn) 641 return node, err 642 } 643 644 func labelStr(l ast.Label) string { 645 switch l := l.(type) { 646 case *ast.Ident: 647 return l.Name 648 case *ast.BasicLit: 649 s, _ := literal.Unquote(l.Value) 650 return s 651 } 652 return "" 653 } 654 655 func isMerge(yn *yaml.Node) bool { 656 // TODO(mvdan): The boolean logic below is from yaml.v3; should we be relying on upstream somehow? 657 return yn.Kind == yaml.ScalarNode && yn.Value == "<<" && (yn.Tag == "" || yn.Tag == "!" || yn.ShortTag() == mergeTag) 658 }