github.com/hashicorp/hcl/v2@v2.20.0/hclwrite/generate.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package hclwrite
     5  
     6  import (
     7  	"fmt"
     8  	"unicode"
     9  	"unicode/utf8"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/hclsyntax"
    13  	"github.com/zclconf/go-cty/cty"
    14  )
    15  
    16  // TokensForValue returns a sequence of tokens that represents the given
    17  // constant value.
    18  //
    19  // This function only supports types that are used by HCL. In particular, it
    20  // does not support capsule types and will panic if given one.
    21  //
    22  // It is not possible to express an unknown value in source code, so this
    23  // function will panic if the given value is unknown or contains any unknown
    24  // values. A caller can call the value's IsWhollyKnown method to verify that
    25  // no unknown values are present before calling TokensForValue.
    26  func TokensForValue(val cty.Value) Tokens {
    27  	toks := appendTokensForValue(val, nil)
    28  	format(toks) // fiddle with the SpacesBefore field to get canonical spacing
    29  	return toks
    30  }
    31  
    32  // TokensForTraversal returns a sequence of tokens that represents the given
    33  // traversal.
    34  //
    35  // If the traversal is absolute then the result is a self-contained, valid
    36  // reference expression. If the traversal is relative then the returned tokens
    37  // could be appended to some other expression tokens to traverse into the
    38  // represented expression.
    39  func TokensForTraversal(traversal hcl.Traversal) Tokens {
    40  	toks := appendTokensForTraversal(traversal, nil)
    41  	format(toks) // fiddle with the SpacesBefore field to get canonical spacing
    42  	return toks
    43  }
    44  
    45  // TokensForIdentifier returns a sequence of tokens representing just the
    46  // given identifier.
    47  //
    48  // In practice this function can only ever generate exactly one token, because
    49  // an identifier is always a leaf token in the syntax tree.
    50  //
    51  // This is similar to calling TokensForTraversal with a single-step absolute
    52  // traversal, but avoids the need to construct a separate traversal object
    53  // for this simple common case. If you need to generate a multi-step traversal,
    54  // use TokensForTraversal instead.
    55  func TokensForIdentifier(name string) Tokens {
    56  	return Tokens{
    57  		newIdentToken(name),
    58  	}
    59  }
    60  
    61  // TokensForTuple returns a sequence of tokens that represents a tuple
    62  // constructor, with element expressions populated from the given list
    63  // of tokens.
    64  //
    65  // TokensForTuple includes the given elements verbatim into the element
    66  // positions in the resulting tuple expression, without any validation to
    67  // ensure that they represent valid expressions. Use TokensForValue or
    68  // TokensForTraversal to generate valid leaf expression values, or use
    69  // TokensForTuple, TokensForObject, and TokensForFunctionCall to
    70  // generate other nested compound expressions.
    71  func TokensForTuple(elems []Tokens) Tokens {
    72  	var toks Tokens
    73  	toks = append(toks, &Token{
    74  		Type:  hclsyntax.TokenOBrack,
    75  		Bytes: []byte{'['},
    76  	})
    77  	for index, elem := range elems {
    78  		if index > 0 {
    79  			toks = append(toks, &Token{
    80  				Type:  hclsyntax.TokenComma,
    81  				Bytes: []byte{','},
    82  			})
    83  		}
    84  		toks = append(toks, elem...)
    85  	}
    86  
    87  	toks = append(toks, &Token{
    88  		Type:  hclsyntax.TokenCBrack,
    89  		Bytes: []byte{']'},
    90  	})
    91  
    92  	format(toks) // fiddle with the SpacesBefore field to get canonical spacing
    93  	return toks
    94  }
    95  
    96  // TokensForObject returns a sequence of tokens that represents an object
    97  // constructor, with attribute name/value pairs populated from the given
    98  // list of attribute token objects.
    99  //
   100  // TokensForObject includes the given tokens verbatim into the name and
   101  // value positions in the resulting object expression, without any validation
   102  // to ensure that they represent valid expressions. Use TokensForValue or
   103  // TokensForTraversal to generate valid leaf expression values, or use
   104  // TokensForTuple, TokensForObject, and TokensForFunctionCall to
   105  // generate other nested compound expressions.
   106  //
   107  // Note that HCL requires placing a traversal expression in parentheses if
   108  // you intend to use it as an attribute name expression, because otherwise
   109  // the parser will interpret it as a literal attribute name. TokensForObject
   110  // does not handle that situation automatically, so a caller must add the
   111  // necessary `TokenOParen` and TokenCParen` manually if needed.
   112  func TokensForObject(attrs []ObjectAttrTokens) Tokens {
   113  	var toks Tokens
   114  	toks = append(toks, &Token{
   115  		Type:  hclsyntax.TokenOBrace,
   116  		Bytes: []byte{'{'},
   117  	})
   118  	if len(attrs) > 0 {
   119  		toks = append(toks, &Token{
   120  			Type:  hclsyntax.TokenNewline,
   121  			Bytes: []byte{'\n'},
   122  		})
   123  	}
   124  	for _, attr := range attrs {
   125  		toks = append(toks, attr.Name...)
   126  		toks = append(toks, &Token{
   127  			Type:  hclsyntax.TokenEqual,
   128  			Bytes: []byte{'='},
   129  		})
   130  		toks = append(toks, attr.Value...)
   131  		toks = append(toks, &Token{
   132  			Type:  hclsyntax.TokenNewline,
   133  			Bytes: []byte{'\n'},
   134  		})
   135  	}
   136  	toks = append(toks, &Token{
   137  		Type:  hclsyntax.TokenCBrace,
   138  		Bytes: []byte{'}'},
   139  	})
   140  
   141  	format(toks) // fiddle with the SpacesBefore field to get canonical spacing
   142  	return toks
   143  }
   144  
   145  // TokensForFunctionCall returns a sequence of tokens that represents call
   146  // to the function with the given name, using the argument tokens to
   147  // populate the argument expressions.
   148  //
   149  // TokensForFunctionCall includes the given argument tokens verbatim into the
   150  // positions in the resulting call expression, without any validation
   151  // to ensure that they represent valid expressions. Use TokensForValue or
   152  // TokensForTraversal to generate valid leaf expression values, or use
   153  // TokensForTuple, TokensForObject, and TokensForFunctionCall to
   154  // generate other nested compound expressions.
   155  //
   156  // This function doesn't include an explicit way to generate the expansion
   157  // symbol "..." on the final argument. Currently, generating that requires
   158  // manually appending a TokenEllipsis with the bytes "..." to the tokens for
   159  // the final argument.
   160  func TokensForFunctionCall(funcName string, args ...Tokens) Tokens {
   161  	var toks Tokens
   162  	toks = append(toks, TokensForIdentifier(funcName)...)
   163  	toks = append(toks, &Token{
   164  		Type:  hclsyntax.TokenOParen,
   165  		Bytes: []byte{'('},
   166  	})
   167  	for index, arg := range args {
   168  		if index > 0 {
   169  			toks = append(toks, &Token{
   170  				Type:  hclsyntax.TokenComma,
   171  				Bytes: []byte{','},
   172  			})
   173  		}
   174  		toks = append(toks, arg...)
   175  	}
   176  	toks = append(toks, &Token{
   177  		Type:  hclsyntax.TokenCParen,
   178  		Bytes: []byte{')'},
   179  	})
   180  
   181  	format(toks) // fiddle with the SpacesBefore field to get canonical spacing
   182  	return toks
   183  }
   184  
   185  func appendTokensForValue(val cty.Value, toks Tokens) Tokens {
   186  	switch {
   187  
   188  	case !val.IsKnown():
   189  		panic("cannot produce tokens for unknown value")
   190  
   191  	case val.IsNull():
   192  		toks = append(toks, &Token{
   193  			Type:  hclsyntax.TokenIdent,
   194  			Bytes: []byte(`null`),
   195  		})
   196  
   197  	case val.Type() == cty.Bool:
   198  		var src []byte
   199  		if val.True() {
   200  			src = []byte(`true`)
   201  		} else {
   202  			src = []byte(`false`)
   203  		}
   204  		toks = append(toks, &Token{
   205  			Type:  hclsyntax.TokenIdent,
   206  			Bytes: src,
   207  		})
   208  
   209  	case val.Type() == cty.Number:
   210  		bf := val.AsBigFloat()
   211  		srcStr := bf.Text('f', -1)
   212  		toks = append(toks, &Token{
   213  			Type:  hclsyntax.TokenNumberLit,
   214  			Bytes: []byte(srcStr),
   215  		})
   216  
   217  	case val.Type() == cty.String:
   218  		// TODO: If it's a multi-line string ending in a newline, format
   219  		// it as a HEREDOC instead.
   220  		src := escapeQuotedStringLit(val.AsString())
   221  		toks = append(toks, &Token{
   222  			Type:  hclsyntax.TokenOQuote,
   223  			Bytes: []byte{'"'},
   224  		})
   225  		if len(src) > 0 {
   226  			toks = append(toks, &Token{
   227  				Type:  hclsyntax.TokenQuotedLit,
   228  				Bytes: src,
   229  			})
   230  		}
   231  		toks = append(toks, &Token{
   232  			Type:  hclsyntax.TokenCQuote,
   233  			Bytes: []byte{'"'},
   234  		})
   235  
   236  	case val.Type().IsListType() || val.Type().IsSetType() || val.Type().IsTupleType():
   237  		toks = append(toks, &Token{
   238  			Type:  hclsyntax.TokenOBrack,
   239  			Bytes: []byte{'['},
   240  		})
   241  
   242  		i := 0
   243  		for it := val.ElementIterator(); it.Next(); {
   244  			if i > 0 {
   245  				toks = append(toks, &Token{
   246  					Type:  hclsyntax.TokenComma,
   247  					Bytes: []byte{','},
   248  				})
   249  			}
   250  			_, eVal := it.Element()
   251  			toks = appendTokensForValue(eVal, toks)
   252  			i++
   253  		}
   254  
   255  		toks = append(toks, &Token{
   256  			Type:  hclsyntax.TokenCBrack,
   257  			Bytes: []byte{']'},
   258  		})
   259  
   260  	case val.Type().IsMapType() || val.Type().IsObjectType():
   261  		toks = append(toks, &Token{
   262  			Type:  hclsyntax.TokenOBrace,
   263  			Bytes: []byte{'{'},
   264  		})
   265  		if val.LengthInt() > 0 {
   266  			toks = append(toks, &Token{
   267  				Type:  hclsyntax.TokenNewline,
   268  				Bytes: []byte{'\n'},
   269  			})
   270  		}
   271  
   272  		i := 0
   273  		for it := val.ElementIterator(); it.Next(); {
   274  			eKey, eVal := it.Element()
   275  			if hclsyntax.ValidIdentifier(eKey.AsString()) {
   276  				toks = append(toks, &Token{
   277  					Type:  hclsyntax.TokenIdent,
   278  					Bytes: []byte(eKey.AsString()),
   279  				})
   280  			} else {
   281  				toks = appendTokensForValue(eKey, toks)
   282  			}
   283  			toks = append(toks, &Token{
   284  				Type:  hclsyntax.TokenEqual,
   285  				Bytes: []byte{'='},
   286  			})
   287  			toks = appendTokensForValue(eVal, toks)
   288  			toks = append(toks, &Token{
   289  				Type:  hclsyntax.TokenNewline,
   290  				Bytes: []byte{'\n'},
   291  			})
   292  			i++
   293  		}
   294  
   295  		toks = append(toks, &Token{
   296  			Type:  hclsyntax.TokenCBrace,
   297  			Bytes: []byte{'}'},
   298  		})
   299  
   300  	default:
   301  		panic(fmt.Sprintf("cannot produce tokens for %#v", val))
   302  	}
   303  
   304  	return toks
   305  }
   306  
   307  func appendTokensForTraversal(traversal hcl.Traversal, toks Tokens) Tokens {
   308  	for _, step := range traversal {
   309  		toks = appendTokensForTraversalStep(step, toks)
   310  	}
   311  	return toks
   312  }
   313  
   314  func appendTokensForTraversalStep(step hcl.Traverser, toks Tokens) Tokens {
   315  	switch ts := step.(type) {
   316  	case hcl.TraverseRoot:
   317  		toks = append(toks, &Token{
   318  			Type:  hclsyntax.TokenIdent,
   319  			Bytes: []byte(ts.Name),
   320  		})
   321  	case hcl.TraverseAttr:
   322  		toks = append(
   323  			toks,
   324  			&Token{
   325  				Type:  hclsyntax.TokenDot,
   326  				Bytes: []byte{'.'},
   327  			},
   328  			&Token{
   329  				Type:  hclsyntax.TokenIdent,
   330  				Bytes: []byte(ts.Name),
   331  			},
   332  		)
   333  	case hcl.TraverseIndex:
   334  		toks = append(toks, &Token{
   335  			Type:  hclsyntax.TokenOBrack,
   336  			Bytes: []byte{'['},
   337  		})
   338  		toks = appendTokensForValue(ts.Key, toks)
   339  		toks = append(toks, &Token{
   340  			Type:  hclsyntax.TokenCBrack,
   341  			Bytes: []byte{']'},
   342  		})
   343  	default:
   344  		panic(fmt.Sprintf("unsupported traversal step type %T", step))
   345  	}
   346  
   347  	return toks
   348  }
   349  
   350  func escapeQuotedStringLit(s string) []byte {
   351  	if len(s) == 0 {
   352  		return nil
   353  	}
   354  	buf := make([]byte, 0, len(s))
   355  	for i, r := range s {
   356  		switch r {
   357  		case '\n':
   358  			buf = append(buf, '\\', 'n')
   359  		case '\r':
   360  			buf = append(buf, '\\', 'r')
   361  		case '\t':
   362  			buf = append(buf, '\\', 't')
   363  		case '"':
   364  			buf = append(buf, '\\', '"')
   365  		case '\\':
   366  			buf = append(buf, '\\', '\\')
   367  		case '$', '%':
   368  			buf = appendRune(buf, r)
   369  			remain := s[i+1:]
   370  			if len(remain) > 0 && remain[0] == '{' {
   371  				// Double up our template introducer symbol to escape it.
   372  				buf = appendRune(buf, r)
   373  			}
   374  		default:
   375  			if !unicode.IsPrint(r) {
   376  				var fmted string
   377  				if r < 65536 {
   378  					fmted = fmt.Sprintf("\\u%04x", r)
   379  				} else {
   380  					fmted = fmt.Sprintf("\\U%08x", r)
   381  				}
   382  				buf = append(buf, fmted...)
   383  			} else {
   384  				buf = appendRune(buf, r)
   385  			}
   386  		}
   387  	}
   388  	return buf
   389  }
   390  
   391  func appendRune(b []byte, r rune) []byte {
   392  	l := utf8.RuneLen(r)
   393  	for i := 0; i < l; i++ {
   394  		b = append(b, 0) // make room at the end of our buffer
   395  	}
   396  	ch := b[len(b)-l:]
   397  	utf8.EncodeRune(ch, r)
   398  	return b
   399  }