cuelang.org/go@v0.13.0/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  	"slices"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  
    15  	"gopkg.in/yaml.v3"
    16  
    17  	"cuelang.org/go/cue/ast"
    18  	"cuelang.org/go/cue/literal"
    19  	"cuelang.org/go/cue/token"
    20  	"cuelang.org/go/internal"
    21  )
    22  
    23  // TODO(mvdan): we should sanity check that the decoder always produces valid CUE,
    24  // as it is possible to construct a cue/ast syntax tree with invalid literals
    25  // or with expressions that will always error, such as `float & 123`.
    26  //
    27  // One option would be to do this as part of the tests; a more general approach
    28  // may be fuzzing, which would find more bugs and work for any decoder,
    29  // although it may be slow as we need to involve the evaluator.
    30  
    31  // Decoder is a temporary interface compatible with both the old and new yaml decoders.
    32  type Decoder interface {
    33  	// Decode consumes a YAML value and returns it in CUE syntax tree node.
    34  	Decode() (ast.Expr, error)
    35  }
    36  
    37  // decoder wraps a [yaml.Decoder] to extract CUE syntax tree nodes.
    38  type decoder struct {
    39  	yamlDecoder yaml.Decoder
    40  
    41  	// yamlNonEmpty is true once yamlDecoder tells us the input YAML wasn't empty.
    42  	// Useful so that we can extract "null" when the input is empty.
    43  	yamlNonEmpty bool
    44  
    45  	// decodeErr is returned by any further calls to Decode when not nil.
    46  	decodeErr error
    47  
    48  	tokFile  *token.File
    49  	tokLines []int
    50  
    51  	// pendingHeadComments collects the head (preceding) comments
    52  	// from the YAML nodes we are extracting.
    53  	// We can't add comments to a CUE syntax tree node until we've created it,
    54  	// but we need to extract these comments first since they have earlier positions.
    55  	pendingHeadComments []*ast.Comment
    56  
    57  	// extractingAliases ensures we don't loop forever when expanding YAML anchors.
    58  	extractingAliases map[*yaml.Node]bool
    59  
    60  	// lastPos is the last YAML node position that we decoded,
    61  	// used for working out relative positions such as token.NewSection.
    62  	// This position can only increase, moving forward in the file.
    63  	lastPos token.Position
    64  
    65  	// forceNewline ensures that the next position will be on a new line.
    66  	forceNewline bool
    67  }
    68  
    69  // TODO(mvdan): this can be io.Reader really, except that token.Pos is offset-based,
    70  // so the only way to really have true Offset+Line+Col numbers is to know
    71  // the size of the entire YAML node upfront.
    72  // With json we can use RawMessage to know the size of the input
    73  // before we extract into ast.Expr, but unfortunately, yaml.Node has no size.
    74  
    75  // NewDecoder creates a decoder for YAML values to extract CUE syntax tree nodes.
    76  //
    77  // The filename is used for position information in CUE syntax tree nodes
    78  // as well as any errors encountered while decoding YAML.
    79  func NewDecoder(filename string, b []byte) *decoder {
    80  	// Note that yaml.v3 can insert a null node just past the end of the input
    81  	// in some edge cases, so we pretend that there's an extra newline
    82  	// so that we don't panic when handling such a position.
    83  	tokFile := token.NewFile(filename, 0, len(b)+1)
    84  	tokFile.SetLinesForContent(b)
    85  	return &decoder{
    86  		tokFile:     tokFile,
    87  		tokLines:    append(tokFile.Lines(), len(b)),
    88  		yamlDecoder: *yaml.NewDecoder(bytes.NewReader(b)),
    89  	}
    90  }
    91  
    92  // Decode consumes a YAML value and returns it in CUE syntax tree node.
    93  //
    94  // A nil node with an io.EOF error is returned once no more YAML values
    95  // are available for decoding.
    96  func (d *decoder) Decode() (ast.Expr, error) {
    97  	if err := d.decodeErr; err != nil {
    98  		return nil, err
    99  	}
   100  	var yn yaml.Node
   101  	if err := d.yamlDecoder.Decode(&yn); err != nil {
   102  		if err == io.EOF {
   103  			// Any further Decode calls must return EOF to avoid an endless loop.
   104  			d.decodeErr = io.EOF
   105  
   106  			// If the input is empty, we produce `*null | _` followed by EOF.
   107  			// Note that when the input contains "---", we get an empty document
   108  			// with a null scalar value inside instead.
   109  			if !d.yamlNonEmpty {
   110  				// Attach positions which at least point to the filename.
   111  				pos := d.tokFile.Pos(0, token.NoRelPos)
   112  				return &ast.BinaryExpr{
   113  					Op:    token.OR,
   114  					OpPos: pos,
   115  					X: &ast.UnaryExpr{
   116  						Op:    token.MUL,
   117  						OpPos: pos,
   118  						X: &ast.BasicLit{
   119  							Kind:     token.NULL,
   120  							ValuePos: pos,
   121  							Value:    "null",
   122  						},
   123  					},
   124  					Y: &ast.Ident{
   125  						Name:    "_",
   126  						NamePos: pos,
   127  					},
   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  
   394  		field := &ast.Field{}
   395  		label, err := d.label(yk)
   396  		if err != nil {
   397  			return err
   398  		}
   399  		d.addCommentsToNode(field, yk, 2)
   400  		field.Label = label
   401  
   402  		if mergeValues {
   403  			key := labelStr(label)
   404  			for _, decl := range m.Elts {
   405  				f := decl.(*ast.Field)
   406  				name, _, err := ast.LabelName(f.Label)
   407  				if err == nil && name == key {
   408  					f.Value, err = d.extract(yv)
   409  					if err != nil {
   410  						return err
   411  					}
   412  					continue outer
   413  				}
   414  			}
   415  		}
   416  
   417  		value, err := d.extract(yv)
   418  		if err != nil {
   419  			return err
   420  		}
   421  		field.Value = value
   422  
   423  		m.Elts = append(m.Elts, field)
   424  	}
   425  	return nil
   426  }
   427  
   428  func (d *decoder) merge(yn *yaml.Node, m *ast.StructLit, multiline bool) error {
   429  	switch yn.Kind {
   430  	case yaml.MappingNode:
   431  		return d.insertMap(yn, m, multiline, true)
   432  	case yaml.AliasNode:
   433  		return d.insertMap(yn.Alias, m, multiline, true)
   434  	case yaml.SequenceNode:
   435  		// Step backwards as earlier nodes take precedence.
   436  		for _, c := range slices.Backward(yn.Content) {
   437  			if err := d.merge(c, m, multiline); err != nil {
   438  				return err
   439  			}
   440  		}
   441  		return nil
   442  	default:
   443  		return d.posErrorf(yn, "map merge requires map or sequence of maps as the value")
   444  	}
   445  }
   446  
   447  func (d *decoder) label(yn *yaml.Node) (ast.Label, error) {
   448  	pos := d.pos(yn)
   449  
   450  	var expr ast.Expr
   451  	var err error
   452  	var value string
   453  	switch yn.Kind {
   454  	case yaml.ScalarNode:
   455  		expr, err = d.scalar(yn)
   456  		value = yn.Value
   457  	case yaml.AliasNode:
   458  		if yn.Alias.Kind != yaml.ScalarNode {
   459  			return nil, d.posErrorf(yn, "invalid map key: %v", yn.Alias.ShortTag())
   460  		}
   461  		expr, err = d.alias(yn)
   462  		value = yn.Alias.Value
   463  	default:
   464  		return nil, d.posErrorf(yn, "invalid map key: %v", yn.ShortTag())
   465  	}
   466  	if err != nil {
   467  		return nil, err
   468  	}
   469  
   470  	switch expr := expr.(type) {
   471  	case *ast.BasicLit:
   472  		if expr.Kind == token.STRING {
   473  			if ast.IsValidIdent(value) && !internal.IsDefOrHidden(value) {
   474  				return &ast.Ident{
   475  					NamePos: pos,
   476  					Name:    value,
   477  				}, nil
   478  			}
   479  			ast.SetPos(expr, pos)
   480  			return expr, nil
   481  		}
   482  
   483  		return &ast.BasicLit{
   484  			ValuePos: pos,
   485  			Kind:     token.STRING,
   486  			Value:    literal.Label.Quote(expr.Value),
   487  		}, nil
   488  
   489  	default:
   490  		return nil, d.posErrorf(yn, "invalid label "+value)
   491  	}
   492  }
   493  
   494  const (
   495  	// TODO(mvdan): The strings below are from yaml.v3; should we be relying on upstream somehow?
   496  	nullTag      = "!!null"
   497  	boolTag      = "!!bool"
   498  	strTag       = "!!str"
   499  	intTag       = "!!int"
   500  	floatTag     = "!!float"
   501  	timestampTag = "!!timestamp"
   502  	seqTag       = "!!seq"
   503  	mapTag       = "!!map"
   504  	binaryTag    = "!!binary"
   505  	mergeTag     = "!!merge"
   506  )
   507  
   508  // rxAnyOctalYaml11 uses the implicit tag resolution regular expression for base-8 integers
   509  // from YAML's 1.1 spec, but including the 8 and 9 digits which aren't valid for octal integers.
   510  var rxAnyOctalYaml11 = sync.OnceValue(func() *regexp.Regexp {
   511  	return regexp.MustCompile(`^[-+]?0[0-9_]+$`)
   512  })
   513  
   514  func (d *decoder) scalar(yn *yaml.Node) (ast.Expr, error) {
   515  	tag := yn.ShortTag()
   516  	// If the YAML scalar has no explicit tag, yaml.v3 infers a float tag,
   517  	// and the value looks like a YAML 1.1 octal literal,
   518  	// that means the input value was like `01289` and not a valid octal integer.
   519  	// The safest thing to do, and what most YAML decoders do, is to interpret as a string.
   520  	if yn.Style&yaml.TaggedStyle == 0 && tag == floatTag && rxAnyOctalYaml11().MatchString(yn.Value) {
   521  		tag = strTag
   522  	}
   523  	switch tag {
   524  	// TODO: use parse literal or parse expression instead.
   525  	case timestampTag:
   526  		return &ast.BasicLit{
   527  			ValuePos: d.pos(yn),
   528  			Kind:     token.STRING,
   529  			Value:    literal.String.Quote(yn.Value),
   530  		}, nil
   531  	case strTag:
   532  		return &ast.BasicLit{
   533  			ValuePos: d.pos(yn),
   534  			Kind:     token.STRING,
   535  			Value:    literal.String.WithOptionalTabIndent(1).Quote(yn.Value),
   536  		}, nil
   537  
   538  	case binaryTag:
   539  		data, err := base64.StdEncoding.DecodeString(yn.Value)
   540  		if err != nil {
   541  			return nil, d.posErrorf(yn, "!!binary value contains invalid base64 data")
   542  		}
   543  		return &ast.BasicLit{
   544  			ValuePos: d.pos(yn),
   545  			Kind:     token.STRING,
   546  			Value:    literal.Bytes.Quote(string(data)),
   547  		}, nil
   548  
   549  	case boolTag:
   550  		t := false
   551  		switch yn.Value {
   552  		// TODO(mvdan): The strings below are from yaml.v3; should we be relying on upstream somehow?
   553  		case "true", "True", "TRUE":
   554  			t = true
   555  		}
   556  		lit := ast.NewBool(t)
   557  		lit.ValuePos = d.pos(yn)
   558  		return lit, nil
   559  
   560  	case intTag:
   561  		// Convert YAML octal to CUE octal. If YAML accepted an invalid
   562  		// integer, just convert it as well to ensure CUE will fail.
   563  		value := yn.Value
   564  		if len(value) > 1 && value[0] == '0' && value[1] <= '9' {
   565  			value = "0o" + value[1:]
   566  		}
   567  		var info literal.NumInfo
   568  		// We make the assumption that any valid YAML integer literal will be a valid
   569  		// CUE integer literal as well, with the only exception of octal numbers above.
   570  		// Note that `!!int 123.456` is not allowed.
   571  		if err := literal.ParseNum(value, &info); err != nil {
   572  			return nil, d.posErrorf(yn, "cannot decode %q as %s: %v", value, tag, err)
   573  		} else if !info.IsInt() {
   574  			return nil, d.posErrorf(yn, "cannot decode %q as %s: not a literal number", value, tag)
   575  		}
   576  		return d.makeNum(yn, value, token.INT), nil
   577  
   578  	case floatTag:
   579  		value := yn.Value
   580  		// TODO(mvdan): The strings below are from yaml.v3; should we be relying on upstream somehow?
   581  		switch value {
   582  		case ".inf", ".Inf", ".INF", "+.inf", "+.Inf", "+.INF":
   583  			value = "+Inf"
   584  		case "-.inf", "-.Inf", "-.INF":
   585  			value = "-Inf"
   586  		case ".nan", ".NaN", ".NAN":
   587  			value = "NaN"
   588  		default:
   589  			var info literal.NumInfo
   590  			// We make the assumption that any valid YAML float literal will be a valid
   591  			// CUE float literal as well, with the only exception of Inf/NaN above.
   592  			// Note that `!!float 123` is allowed.
   593  			if err := literal.ParseNum(value, &info); err != nil {
   594  				return nil, d.posErrorf(yn, "cannot decode %q as %s: %v", value, tag, err)
   595  			}
   596  			// If the decoded YAML scalar was explicitly or implicitly a float,
   597  			// and the scalar literal looks like an integer,
   598  			// unify it with "number" to record the fact that it was represented as a float.
   599  			// Don't unify with float, as `float & 123` is invalid, and there's no need
   600  			// to forbid representing the number as an integer either.
   601  			if yn.Tag != "" {
   602  				if p := strings.IndexAny(value, ".eEiInN"); p == -1 {
   603  					// TODO: number(v) when we have conversions
   604  					// TODO(mvdan): don't shove the unification inside a BasicLit.Value string
   605  					//
   606  					// TODO(mvdan): would it be better to do turn `!!float 123` into `123.0`
   607  					// rather than `number & 123`? Note that `float & 123` is an error.
   608  					value = fmt.Sprintf("number & %s", value)
   609  				}
   610  			}
   611  		}
   612  		return d.makeNum(yn, value, token.FLOAT), nil
   613  
   614  	case nullTag:
   615  		return &ast.BasicLit{
   616  			ValuePos: d.pos(yn).WithRel(token.Blank),
   617  			Kind:     token.NULL,
   618  			Value:    "null",
   619  		}, nil
   620  	default:
   621  		return nil, d.posErrorf(yn, "cannot unmarshal tag %q", tag)
   622  	}
   623  }
   624  
   625  func (d *decoder) makeNum(yn *yaml.Node, val string, kind token.Token) (expr ast.Expr) {
   626  	val, negative := strings.CutPrefix(val, "-")
   627  	expr = &ast.BasicLit{
   628  		ValuePos: d.pos(yn),
   629  		Kind:     kind,
   630  		Value:    val,
   631  	}
   632  	if negative {
   633  		expr = &ast.UnaryExpr{
   634  			OpPos: d.pos(yn),
   635  			Op:    token.SUB,
   636  			X:     expr,
   637  		}
   638  	}
   639  	return expr
   640  }
   641  
   642  func (d *decoder) alias(yn *yaml.Node) (ast.Expr, error) {
   643  	if d.extractingAliases[yn] {
   644  		// TODO this could actually be allowed in some circumstances.
   645  		return nil, d.posErrorf(yn, "anchor %q value contains itself", yn.Value)
   646  	}
   647  	if d.extractingAliases == nil {
   648  		d.extractingAliases = make(map[*yaml.Node]bool)
   649  	}
   650  	d.extractingAliases[yn] = true
   651  	var node ast.Expr
   652  	node, err := d.extract(yn.Alias)
   653  	delete(d.extractingAliases, yn)
   654  	return node, err
   655  }
   656  
   657  func labelStr(l ast.Label) string {
   658  	switch l := l.(type) {
   659  	case *ast.Ident:
   660  		return l.Name
   661  	case *ast.BasicLit:
   662  		s, _ := literal.Unquote(l.Value)
   663  		return s
   664  	}
   665  	return ""
   666  }
   667  
   668  func isMerge(yn *yaml.Node) bool {
   669  	// TODO(mvdan): The boolean logic below is from yaml.v3; should we be relying on upstream somehow?
   670  	return yn.Kind == yaml.ScalarNode && yn.Value == "<<" && (yn.Tag == "" || yn.Tag == "!" || yn.ShortTag() == mergeTag)
   671  }