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  }