github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/configupgrade/upgrade_expr.go (about)

     1  package configupgrade
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"strconv"
     8  	"strings"
     9  
    10  	hcl2 "github.com/hashicorp/hcl/v2"
    11  	hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
    12  	"github.com/zclconf/go-cty/cty"
    13  
    14  	hcl1ast "github.com/hashicorp/hcl/hcl/ast"
    15  	hcl1printer "github.com/hashicorp/hcl/hcl/printer"
    16  	hcl1token "github.com/hashicorp/hcl/hcl/token"
    17  
    18  	"github.com/hashicorp/hil"
    19  	hilast "github.com/hashicorp/hil/ast"
    20  
    21  	"github.com/hashicorp/terraform/addrs"
    22  	"github.com/hashicorp/terraform/configs/configschema"
    23  	"github.com/hashicorp/terraform/tfdiags"
    24  )
    25  
    26  func upgradeExpr(val interface{}, filename string, interp bool, an *analysis) ([]byte, tfdiags.Diagnostics) {
    27  	var buf bytes.Buffer
    28  	var diags tfdiags.Diagnostics
    29  
    30  	// "val" here can be either a hcl1ast.Node or a hilast.Node, since both
    31  	// of these correspond to expressions in HCL2. Therefore we need to
    32  	// comprehensively handle every possible HCL1 *and* HIL AST node type
    33  	// and, at minimum, print it out as-is in HCL2 syntax.
    34  Value:
    35  	switch tv := val.(type) {
    36  
    37  	case *hcl1ast.LiteralType:
    38  		return upgradeExpr(tv.Token, filename, interp, an)
    39  
    40  	case hcl1token.Token:
    41  		switch tv.Type {
    42  		case hcl1token.STRING:
    43  			litVal := tv.Value()
    44  			if !interp {
    45  				// Easy case, then.
    46  				printQuotedString(&buf, litVal.(string))
    47  				break
    48  			}
    49  
    50  			hilNode, err := hil.Parse(litVal.(string))
    51  			if err != nil {
    52  				diags = diags.Append(&hcl2.Diagnostic{
    53  					Severity: hcl2.DiagError,
    54  					Summary:  "Invalid interpolated string",
    55  					Detail:   fmt.Sprintf("Interpolation parsing failed: %s", err),
    56  					Subject:  hcl1PosRange(filename, tv.Pos).Ptr(),
    57  				})
    58  				return nil, diags
    59  			}
    60  
    61  			interpSrc, interpDiags := upgradeExpr(hilNode, filename, interp, an)
    62  			buf.Write(interpSrc)
    63  			diags = diags.Append(interpDiags)
    64  
    65  		case hcl1token.HEREDOC:
    66  			// HCL1's "Value" method for tokens pulls out the body and removes
    67  			// any indents in the source for a flush heredoc, which throws away
    68  			// information we need to upgrade. Therefore we're going to
    69  			// re-implement a subset of that logic here where we want to retain
    70  			// the whitespace verbatim even in flush mode.
    71  
    72  			firstNewlineIdx := strings.IndexByte(tv.Text, '\n')
    73  			if firstNewlineIdx < 0 {
    74  				// Should never happen, because tv.Value would already have
    75  				// panicked above in this case.
    76  				panic("heredoc doesn't contain newline")
    77  			}
    78  			introducer := tv.Text[:firstNewlineIdx+1]
    79  			marker := introducer[2:] // trim off << prefix
    80  			if marker[0] == '-' {
    81  				marker = marker[1:] // also trim of - prefix for flush heredoc
    82  			}
    83  			body := tv.Text[len(introducer) : len(tv.Text)-len(marker)]
    84  			flush := introducer[2] == '-'
    85  			if flush {
    86  				// HCL1 treats flush heredocs differently, trimming off any
    87  				// spare whitespace that might appear after the trailing
    88  				// newline, and so we must replicate that here to avoid
    89  				// introducing additional whitespace in the output.
    90  				body = strings.TrimRight(body, " \t")
    91  			}
    92  
    93  			// Now we have:
    94  			//  - introducer is the first line, like "<<-FOO\n"
    95  			//  - marker is the end marker, like "FOO\n"
    96  			//  - body is the raw data between the introducer and the marker,
    97  			//    which we need to do recursive upgrading for.
    98  
    99  			buf.WriteString(introducer)
   100  			if !interp {
   101  				// Easy case: escape all interpolation-looking sequences.
   102  				printHeredocLiteralFromHILOutput(&buf, body)
   103  			} else {
   104  				hilNode, err := hil.Parse(body)
   105  				if err != nil {
   106  					diags = diags.Append(&hcl2.Diagnostic{
   107  						Severity: hcl2.DiagError,
   108  						Summary:  "Invalid interpolated string",
   109  						Detail:   fmt.Sprintf("Interpolation parsing failed: %s", err),
   110  						Subject:  hcl1PosRange(filename, tv.Pos).Ptr(),
   111  					})
   112  				}
   113  				if hilNode != nil {
   114  					if _, ok := hilNode.(*hilast.Output); !ok {
   115  						// hil.Parse usually produces an output, but it can sometimes
   116  						// produce an isolated expression if the input is entirely
   117  						// a single interpolation.
   118  						if hilNode != nil {
   119  							hilNode = &hilast.Output{
   120  								Exprs: []hilast.Node{hilNode},
   121  								Posx:  hilNode.Pos(),
   122  							}
   123  						}
   124  					}
   125  					interpDiags := upgradeHeredocBody(&buf, hilNode.(*hilast.Output), filename, an)
   126  					diags = diags.Append(interpDiags)
   127  				}
   128  			}
   129  			if !strings.HasSuffix(body, "\n") {
   130  				// The versions of HCL1 vendored into Terraform <=0.11
   131  				// incorrectly allowed the end marker to appear at the end of
   132  				// the final line of the body, rather than on a line of its own.
   133  				// That is no longer valid in HCL2, so we need to fix it up.
   134  				buf.WriteByte('\n')
   135  			}
   136  			// NOTE: Marker intentionally contains an extra newline here because
   137  			// we need to ensure that any follow-on expression bits end up on
   138  			// a separate line, or else the HCL2 parser won't be able to
   139  			// recognize the heredoc marker. This causes an extra empty line
   140  			// in some cases, which we accept for simplicity's sake.
   141  			buf.WriteString(marker)
   142  
   143  		case hcl1token.BOOL:
   144  			litVal := tv.Value()
   145  			if litVal.(bool) {
   146  				buf.WriteString("true")
   147  			} else {
   148  				buf.WriteString("false")
   149  			}
   150  
   151  		case hcl1token.NUMBER:
   152  			num, err := strconv.ParseInt(tv.Text, 0, 64)
   153  			if err != nil {
   154  				diags = diags.Append(&hcl2.Diagnostic{
   155  					Severity: hcl2.DiagError,
   156  					Summary:  "Invalid number value",
   157  					Detail:   fmt.Sprintf("Parsing failed: %s", err),
   158  					Subject:  hcl1PosRange(filename, tv.Pos).Ptr(),
   159  				})
   160  			}
   161  			buf.WriteString(strconv.FormatInt(num, 10))
   162  
   163  		case hcl1token.FLOAT:
   164  			num, err := strconv.ParseFloat(tv.Text, 64)
   165  			if err != nil {
   166  				diags = diags.Append(&hcl2.Diagnostic{
   167  					Severity: hcl2.DiagError,
   168  					Summary:  "Invalid float value",
   169  					Detail:   fmt.Sprintf("Parsing failed: %s", err),
   170  					Subject:  hcl1PosRange(filename, tv.Pos).Ptr(),
   171  				})
   172  			}
   173  			buf.WriteString(strconv.FormatFloat(num, 'f', -1, 64))
   174  
   175  		default:
   176  			// For everything else we'll just pass through the given bytes verbatim,
   177  			// but we should't get here because the above is intended to be exhaustive.
   178  			buf.WriteString(tv.Text)
   179  
   180  		}
   181  
   182  	case *hcl1ast.ListType:
   183  		multiline := tv.Lbrack.Line != tv.Rbrack.Line
   184  		buf.WriteString("[")
   185  		if multiline {
   186  			buf.WriteString("\n")
   187  		}
   188  		for i, node := range tv.List {
   189  			src, moreDiags := upgradeExpr(node, filename, interp, an)
   190  			diags = diags.Append(moreDiags)
   191  			buf.Write(src)
   192  			if lit, ok := node.(*hcl1ast.LiteralType); ok && lit.LineComment != nil {
   193  				for _, comment := range lit.LineComment.List {
   194  					buf.WriteString(", " + comment.Text)
   195  					buf.WriteString("\n")
   196  				}
   197  			} else {
   198  				if multiline {
   199  					buf.WriteString(",\n")
   200  				} else if i < len(tv.List)-1 {
   201  					buf.WriteString(", ")
   202  				}
   203  			}
   204  		}
   205  		buf.WriteString("]")
   206  
   207  	case *hcl1ast.ObjectType:
   208  		if len(tv.List.Items) == 0 {
   209  			buf.WriteString("{}")
   210  			break
   211  		}
   212  		buf.WriteString("{\n")
   213  		for _, item := range tv.List.Items {
   214  			if len(item.Keys) != 1 {
   215  				diags = diags.Append(&hcl2.Diagnostic{
   216  					Severity: hcl2.DiagError,
   217  					Summary:  "Invalid map element",
   218  					Detail:   "A map element may not have any block-style labels.",
   219  					Subject:  hcl1PosRange(filename, item.Pos()).Ptr(),
   220  				})
   221  				continue
   222  			}
   223  			keySrc, moreDiags := upgradeExpr(item.Keys[0].Token, filename, interp, an)
   224  			diags = diags.Append(moreDiags)
   225  			valueSrc, moreDiags := upgradeExpr(item.Val, filename, interp, an)
   226  			diags = diags.Append(moreDiags)
   227  			if item.LeadComment != nil {
   228  				for _, c := range item.LeadComment.List {
   229  					buf.WriteString(c.Text)
   230  					buf.WriteByte('\n')
   231  				}
   232  			}
   233  
   234  			buf.Write(keySrc)
   235  			buf.WriteString(" = ")
   236  			buf.Write(valueSrc)
   237  			if item.LineComment != nil {
   238  				for _, c := range item.LineComment.List {
   239  					buf.WriteByte(' ')
   240  					buf.WriteString(c.Text)
   241  				}
   242  			}
   243  			buf.WriteString("\n")
   244  		}
   245  		buf.WriteString("}")
   246  
   247  	case hcl1ast.Node:
   248  		// If our more-specific cases above didn't match this then we'll
   249  		// ask the hcl1printer package to print the expression out
   250  		// itself, and assume it'll still be valid in HCL2.
   251  		// (We should rarely end up here, since our cases above should
   252  		// be comprehensive.)
   253  		log.Printf("[TRACE] configupgrade: Don't know how to upgrade %T as expression, so just passing it through as-is", tv)
   254  		hcl1printer.Fprint(&buf, tv)
   255  
   256  	case *hilast.LiteralNode:
   257  		switch tl := tv.Value.(type) {
   258  		case string:
   259  			// This shouldn't generally happen because literal strings are
   260  			// always wrapped in hilast.Output in HIL, but we'll allow it anyway.
   261  			printQuotedString(&buf, tl)
   262  		case int:
   263  			buf.WriteString(strconv.Itoa(tl))
   264  		case float64:
   265  			buf.WriteString(strconv.FormatFloat(tl, 'f', -1, 64))
   266  		case bool:
   267  			if tl {
   268  				buf.WriteString("true")
   269  			} else {
   270  				buf.WriteString("false")
   271  			}
   272  		}
   273  
   274  	case *hilast.VariableAccess:
   275  		// In HIL a variable access is just a single string which might contain
   276  		// a mixture of identifiers, dots, integer indices, and splat expressions.
   277  		// All of these concepts were formerly interpreted by Terraform itself,
   278  		// rather than by HIL. We're going to process this one chunk at a time
   279  		// here so we can normalize and introduce some newer syntax where it's
   280  		// safe to do so.
   281  		parts := strings.Split(tv.Name, ".")
   282  
   283  		transformed := transformCountPseudoAttribute(&buf, parts, an)
   284  		if transformed {
   285  			break Value
   286  		}
   287  
   288  		parts = upgradeTraversalParts(parts, an) // might add/remove/change parts
   289  
   290  		vDiags := validateHilAddress(tv.Name, filename)
   291  		if len(vDiags) > 0 {
   292  			diags = diags.Append(vDiags)
   293  			break
   294  		}
   295  
   296  		printHilTraversalPartsAsHcl2(&buf, parts)
   297  
   298  	case *hilast.Arithmetic:
   299  		op, exists := hilArithmeticOpSyms[tv.Op]
   300  		if !exists {
   301  			panic(fmt.Errorf("arithmetic node with unsupported operator %#v", tv.Op))
   302  		}
   303  
   304  		lhsExpr := tv.Exprs[0]
   305  		rhsExpr := tv.Exprs[1]
   306  		lhsSrc, exprDiags := upgradeExpr(lhsExpr, filename, true, an)
   307  		diags = diags.Append(exprDiags)
   308  		rhsSrc, exprDiags := upgradeExpr(rhsExpr, filename, true, an)
   309  		diags = diags.Append(exprDiags)
   310  
   311  		// HIL's AST represents -foo as (0 - foo), so we'll recognize
   312  		// that here and normalize it back.
   313  		if tv.Op == hilast.ArithmeticOpSub && len(lhsSrc) == 1 && lhsSrc[0] == '0' {
   314  			buf.WriteString("-")
   315  			buf.Write(rhsSrc)
   316  			break
   317  		}
   318  
   319  		buf.Write(lhsSrc)
   320  		buf.WriteString(op)
   321  		buf.Write(rhsSrc)
   322  
   323  	case *hilast.Call:
   324  		name := tv.Func
   325  		args := tv.Args
   326  
   327  		// Some adaptations must happen prior to upgrading the arguments,
   328  		// because they depend on the original argument AST nodes.
   329  		switch name {
   330  		case "base64sha256", "base64sha512", "md5", "sha1", "sha256", "sha512":
   331  			// These functions were sometimes used in conjunction with the
   332  			// file() function to take the hash of the contents of a file.
   333  			// Prior to Terraform 0.11 there was a chance of silent corruption
   334  			// of strings containing non-UTF8 byte sequences, and so we have
   335  			// made it illegal to use file() with non-text files in 0.12 even
   336  			// though in this _particular_ situation (passing the function
   337  			// result directly to another function) there would not be any
   338  			// corruption; the general rule keeps things consistent.
   339  			// However, to still meet those use-cases we now have variants of
   340  			// the hashing functions that have a "file" prefix on their names
   341  			// and read the contents of a given file, rather than hashing
   342  			// directly the given string.
   343  			if len(args) > 0 {
   344  				if subCall, ok := args[0].(*hilast.Call); ok && subCall.Func == "file" {
   345  					// We're going to flatten this down into a single call, so
   346  					// we actually want the arguments of the sub-call here.
   347  					name = "file" + name
   348  					args = subCall.Args
   349  
   350  					// For this one, we'll fall through to the normal upgrade
   351  					// handling now that we've fixed up the name and args...
   352  				}
   353  			}
   354  
   355  		}
   356  
   357  		argExprs := make([][]byte, len(args))
   358  		multiline := false
   359  		totalLen := 0
   360  		for i, arg := range args {
   361  			if i > 0 {
   362  				totalLen += 2
   363  			}
   364  			exprSrc, exprDiags := upgradeExpr(arg, filename, true, an)
   365  			diags = diags.Append(exprDiags)
   366  			argExprs[i] = exprSrc
   367  			if bytes.Contains(exprSrc, []byte{'\n'}) {
   368  				// If any of our arguments are multi-line then we'll also be multiline
   369  				multiline = true
   370  			}
   371  			totalLen += len(exprSrc)
   372  		}
   373  
   374  		if totalLen > 60 { // heuristic, since we don't know here how indented we are already
   375  			multiline = true
   376  		}
   377  
   378  		// Some functions are now better expressed as native language constructs.
   379  		// These cases will return early if they emit anything, or otherwise
   380  		// fall through to the default emitter.
   381  		switch name {
   382  		case "list":
   383  			// Should now use tuple constructor syntax
   384  			buf.WriteByte('[')
   385  			if multiline {
   386  				buf.WriteByte('\n')
   387  			}
   388  			for i, exprSrc := range argExprs {
   389  				buf.Write(exprSrc)
   390  				if multiline {
   391  					buf.WriteString(",\n")
   392  				} else {
   393  					if i < len(args)-1 {
   394  						buf.WriteString(", ")
   395  					}
   396  				}
   397  			}
   398  			buf.WriteByte(']')
   399  			break Value
   400  		case "map":
   401  			// Should now use object constructor syntax, but we can only
   402  			// achieve that if the call is valid, which requires an even
   403  			// number of arguments.
   404  			if len(argExprs) == 0 {
   405  				buf.WriteString("{}")
   406  				break Value
   407  			} else if len(argExprs)%2 == 0 {
   408  				buf.WriteString("{\n")
   409  				for i := 0; i < len(argExprs); i += 2 {
   410  					k := argExprs[i]
   411  					v := argExprs[i+1]
   412  
   413  					buf.Write(k)
   414  					buf.WriteString(" = ")
   415  					buf.Write(v)
   416  					buf.WriteByte('\n')
   417  				}
   418  				buf.WriteByte('}')
   419  				break Value
   420  			}
   421  		case "lookup":
   422  			// A lookup call with only two arguments is equivalent to native
   423  			// index syntax. (A third argument would specify a default value,
   424  			// so calls like that must be left alone.)
   425  			// (Note that we can't safely do this for element(...) because
   426  			// the user may be relying on its wraparound behavior.)
   427  			if len(argExprs) == 2 {
   428  				buf.Write(argExprs[0])
   429  				buf.WriteByte('[')
   430  				buf.Write(argExprs[1])
   431  				buf.WriteByte(']')
   432  				break Value
   433  			}
   434  		case "element":
   435  			// We cannot replace element with index syntax safely in general
   436  			// because users may be relying on its special modulo wraparound
   437  			// behavior that the index syntax doesn't do. However, if it seems
   438  			// like the user is trying to use element with a set, we'll insert
   439  			// an explicit conversion to list to mimic the implicit conversion
   440  			// that we used to do as an unintended side-effect of how functions
   441  			// work in HIL.
   442  			if len(argExprs) > 0 {
   443  				argTy := an.InferExpressionType(argExprs[0], nil)
   444  				if argTy.IsSetType() {
   445  					newExpr := []byte(`tolist(`)
   446  					newExpr = append(newExpr, argExprs[0]...)
   447  					newExpr = append(newExpr, ')')
   448  					argExprs[0] = newExpr
   449  				}
   450  			}
   451  
   452  			// HIL used some undocumented special functions to implement certain
   453  			// operations, but since those were actually callable in real expressions
   454  			// some users inevitably depended on them, so we'll fix them up here.
   455  			// These each become two function calls to preserve the old behavior
   456  			// of implicitly converting to the source type first. Usage of these
   457  			// is relatively rare, so the result doesn't need to be too pretty.
   458  		case "__builtin_BoolToString":
   459  			buf.WriteString("tostring(tobool(")
   460  			buf.Write(argExprs[0])
   461  			buf.WriteString("))")
   462  			break Value
   463  		case "__builtin_FloatToString":
   464  			buf.WriteString("tostring(tonumber(")
   465  			buf.Write(argExprs[0])
   466  			buf.WriteString("))")
   467  			break Value
   468  		case "__builtin_IntToString":
   469  			buf.WriteString("tostring(floor(")
   470  			buf.Write(argExprs[0])
   471  			buf.WriteString("))")
   472  			break Value
   473  		case "__builtin_StringToInt":
   474  			buf.WriteString("floor(tostring(")
   475  			buf.Write(argExprs[0])
   476  			buf.WriteString("))")
   477  			break Value
   478  		case "__builtin_StringToFloat":
   479  			buf.WriteString("tonumber(tostring(")
   480  			buf.Write(argExprs[0])
   481  			buf.WriteString("))")
   482  			break Value
   483  		case "__builtin_StringToBool":
   484  			buf.WriteString("tobool(tostring(")
   485  			buf.Write(argExprs[0])
   486  			buf.WriteString("))")
   487  			break Value
   488  		case "__builtin_FloatToInt", "__builtin_IntToFloat":
   489  			// Since "floor" already has an implicit conversion of its argument
   490  			// to number, and the result is a whole number in either case,
   491  			// these ones are easier. (We no longer distinguish int and float
   492  			// as types in HCL2, even though HIL did.)
   493  			name = "floor"
   494  		}
   495  
   496  		buf.WriteString(name)
   497  		buf.WriteByte('(')
   498  		if multiline {
   499  			buf.WriteByte('\n')
   500  		}
   501  		for i, exprSrc := range argExprs {
   502  			buf.Write(exprSrc)
   503  			if multiline {
   504  				buf.WriteString(",\n")
   505  			} else {
   506  				if i < len(args)-1 {
   507  					buf.WriteString(", ")
   508  				}
   509  			}
   510  		}
   511  		buf.WriteByte(')')
   512  
   513  	case *hilast.Conditional:
   514  		condSrc, exprDiags := upgradeExpr(tv.CondExpr, filename, true, an)
   515  		diags = diags.Append(exprDiags)
   516  		trueSrc, exprDiags := upgradeExpr(tv.TrueExpr, filename, true, an)
   517  		diags = diags.Append(exprDiags)
   518  		falseSrc, exprDiags := upgradeExpr(tv.FalseExpr, filename, true, an)
   519  		diags = diags.Append(exprDiags)
   520  
   521  		buf.Write(condSrc)
   522  		buf.WriteString(" ? ")
   523  		buf.Write(trueSrc)
   524  		buf.WriteString(" : ")
   525  		buf.Write(falseSrc)
   526  
   527  	case *hilast.Index:
   528  		target, ok := tv.Target.(*hilast.VariableAccess)
   529  		if !ok {
   530  			panic(fmt.Sprintf("Index node with unsupported target type (%T)", tv.Target))
   531  		}
   532  		parts := strings.Split(target.Name, ".")
   533  
   534  		keySrc, exprDiags := upgradeExpr(tv.Key, filename, true, an)
   535  		diags = diags.Append(exprDiags)
   536  
   537  		transformed := transformCountPseudoAttribute(&buf, parts, an)
   538  		if transformed {
   539  			break Value
   540  		}
   541  
   542  		parts = upgradeTraversalParts(parts, an) // might add/remove/change parts
   543  
   544  		vDiags := validateHilAddress(target.Name, filename)
   545  		if len(vDiags) > 0 {
   546  			diags = diags.Append(vDiags)
   547  			break
   548  		}
   549  
   550  		first, remain := parts[0], parts[1:]
   551  
   552  		var rAddr addrs.Resource
   553  		switch parts[0] {
   554  		case "data":
   555  			if len(parts) == 5 && parts[3] == "*" {
   556  				rAddr.Mode = addrs.DataResourceMode
   557  				rAddr.Type = parts[1]
   558  				rAddr.Name = parts[2]
   559  			}
   560  		default:
   561  			if len(parts) == 4 && parts[2] == "*" {
   562  				rAddr.Mode = addrs.ManagedResourceMode
   563  				rAddr.Type = parts[0]
   564  				rAddr.Name = parts[1]
   565  			}
   566  		}
   567  
   568  		// We need to check if the thing being referenced has count
   569  		// to retain backward compatibility
   570  		hasCount := false
   571  		if v, exists := an.ResourceHasCount[rAddr]; exists {
   572  			hasCount = v
   573  		}
   574  
   575  		hasSplat := false
   576  
   577  		buf.WriteString(first)
   578  		for _, part := range remain {
   579  			// Attempt to convert old-style splat indexing to new one
   580  			// e.g. res.label.*.attr[idx] to res.label[idx].attr
   581  			if part == "*" && hasCount {
   582  				hasSplat = true
   583  				buf.WriteString(fmt.Sprintf("[%s]", keySrc))
   584  				continue
   585  			}
   586  
   587  			buf.WriteByte('.')
   588  			buf.WriteString(part)
   589  		}
   590  
   591  		if !hasSplat {
   592  			buf.WriteString("[")
   593  			buf.Write(keySrc)
   594  			buf.WriteString("]")
   595  		}
   596  
   597  	case *hilast.Output:
   598  		if len(tv.Exprs) == 1 {
   599  			item := tv.Exprs[0]
   600  			naked := true
   601  			if lit, ok := item.(*hilast.LiteralNode); ok {
   602  				if _, ok := lit.Value.(string); ok {
   603  					naked = false
   604  				}
   605  			}
   606  			if naked {
   607  				// If there's only one expression and it isn't a literal string
   608  				// then we'll just output it naked, since wrapping a single
   609  				// expression in interpolation is no longer idiomatic.
   610  				interped, interpDiags := upgradeExpr(item, filename, true, an)
   611  				diags = diags.Append(interpDiags)
   612  				buf.Write(interped)
   613  				break
   614  			}
   615  		}
   616  
   617  		buf.WriteString(`"`)
   618  		for _, item := range tv.Exprs {
   619  			if lit, ok := item.(*hilast.LiteralNode); ok {
   620  				if litStr, ok := lit.Value.(string); ok {
   621  					printStringLiteralFromHILOutput(&buf, litStr)
   622  					continue
   623  				}
   624  			}
   625  
   626  			interped, interpDiags := upgradeExpr(item, filename, true, an)
   627  			diags = diags.Append(interpDiags)
   628  
   629  			buf.WriteString("${")
   630  			buf.Write(interped)
   631  			buf.WriteString("}")
   632  		}
   633  		buf.WriteString(`"`)
   634  
   635  	case hilast.Node:
   636  		// Nothing reasonable we can do here, so we should've handled all of
   637  		// the possibilities above.
   638  		panic(fmt.Errorf("upgradeExpr doesn't handle HIL node type %T", tv))
   639  
   640  	default:
   641  		// If we end up in here then the caller gave us something completely invalid.
   642  		panic(fmt.Errorf("upgradeExpr on unsupported type %T", val))
   643  
   644  	}
   645  
   646  	return buf.Bytes(), diags
   647  }
   648  
   649  func validateHilAddress(address, filename string) tfdiags.Diagnostics {
   650  	parts := strings.Split(address, ".")
   651  	var diags tfdiags.Diagnostics
   652  
   653  	label, ok := getResourceLabel(parts)
   654  	if ok && !hcl2syntax.ValidIdentifier(label) {
   655  		// We can't get any useful source location out of HIL unfortunately
   656  		diags = diags.Append(tfdiags.Sourceless(
   657  			tfdiags.Error,
   658  			fmt.Sprintf("Invalid address (%s) in ./%s", address, filename),
   659  			// The label could be invalid for another reason
   660  			// but this is the most likely, so we add it as hint
   661  			"Names of objects (resources, modules, etc) may no longer start with digits."))
   662  	}
   663  
   664  	return diags
   665  }
   666  
   667  func getResourceLabel(parts []string) (string, bool) {
   668  	if len(parts) < 1 {
   669  		return "", false
   670  	}
   671  
   672  	if parts[0] == "data" {
   673  		if len(parts) < 3 {
   674  			return "", false
   675  		}
   676  		return parts[2], true
   677  	}
   678  
   679  	if len(parts) < 2 {
   680  		return "", false
   681  	}
   682  
   683  	return parts[1], true
   684  }
   685  
   686  // transformCountPseudoAttribute deals with the .count pseudo-attributes
   687  // that 0.11 and prior allowed for resources. These no longer exist,
   688  // because they don't do anything we can't do with the length(...) function.
   689  func transformCountPseudoAttribute(buf *bytes.Buffer, parts []string, an *analysis) (transformed bool) {
   690  	if len(parts) > 0 {
   691  		var rAddr addrs.Resource
   692  		switch parts[0] {
   693  		case "data":
   694  			if len(parts) == 4 && parts[3] == "count" {
   695  				rAddr.Mode = addrs.DataResourceMode
   696  				rAddr.Type = parts[1]
   697  				rAddr.Name = parts[2]
   698  			}
   699  		default:
   700  			if len(parts) == 3 && parts[2] == "count" {
   701  				rAddr.Mode = addrs.ManagedResourceMode
   702  				rAddr.Type = parts[0]
   703  				rAddr.Name = parts[1]
   704  			}
   705  		}
   706  		// We need to check if the thing being referenced is actually an
   707  		// existing resource, because other three-part traversals might
   708  		// coincidentally end with "count".
   709  		if hasCount, exists := an.ResourceHasCount[rAddr]; exists {
   710  			if hasCount {
   711  				buf.WriteString("length(")
   712  				buf.WriteString(rAddr.String())
   713  				buf.WriteString(")")
   714  			} else {
   715  				// If the resource does not have count, the .count
   716  				// attr would've always returned 1 before.
   717  				buf.WriteString("1")
   718  			}
   719  			transformed = true
   720  			return
   721  		}
   722  	}
   723  	return
   724  }
   725  
   726  func printHilTraversalPartsAsHcl2(buf *bytes.Buffer, parts []string) {
   727  	first, remain := parts[0], parts[1:]
   728  	buf.WriteString(first)
   729  	seenSplat := false
   730  	for _, part := range remain {
   731  		if part == "*" {
   732  			seenSplat = true
   733  			buf.WriteString(".*")
   734  			continue
   735  		}
   736  
   737  		// Other special cases apply only if we've not previously
   738  		// seen a splat expression marker, since attribute vs. index
   739  		// syntax have different interpretations after a simple splat.
   740  		if !seenSplat {
   741  			if v, err := strconv.Atoi(part); err == nil {
   742  				// Looks like it's old-style index traversal syntax foo.0.bar
   743  				// so we'll replace with canonical index syntax foo[0].bar.
   744  				fmt.Fprintf(buf, "[%d]", v)
   745  				continue
   746  			}
   747  			if !hcl2syntax.ValidIdentifier(part) {
   748  				// This should be rare since HIL's identifier syntax is _close_
   749  				// to HCL2's, but we'll get here if one of the intervening
   750  				// parts is not a valid identifier in isolation, since HIL
   751  				// did not consider these to be separate identifiers.
   752  				// e.g. foo.1bar would be invalid in HCL2; must instead be foo["1bar"].
   753  				buf.WriteByte('[')
   754  				printQuotedString(buf, part)
   755  				buf.WriteByte(']')
   756  				continue
   757  			}
   758  		}
   759  
   760  		buf.WriteByte('.')
   761  		buf.WriteString(part)
   762  	}
   763  }
   764  
   765  func upgradeHeredocBody(buf *bytes.Buffer, val *hilast.Output, filename string, an *analysis) tfdiags.Diagnostics {
   766  	var diags tfdiags.Diagnostics
   767  
   768  	for _, item := range val.Exprs {
   769  		if lit, ok := item.(*hilast.LiteralNode); ok {
   770  			if litStr, ok := lit.Value.(string); ok {
   771  				printHeredocLiteralFromHILOutput(buf, litStr)
   772  				continue
   773  			}
   774  		}
   775  		interped, interpDiags := upgradeExpr(item, filename, true, an)
   776  		diags = diags.Append(interpDiags)
   777  
   778  		buf.WriteString("${")
   779  		buf.Write(interped)
   780  		buf.WriteString("}")
   781  	}
   782  
   783  	return diags
   784  }
   785  
   786  func upgradeTraversalExpr(val interface{}, filename string, an *analysis) ([]byte, tfdiags.Diagnostics) {
   787  	if lit, ok := val.(*hcl1ast.LiteralType); ok && lit.Token.Type == hcl1token.STRING {
   788  		trStr := lit.Token.Value().(string)
   789  		if strings.HasSuffix(trStr, ".%") || strings.HasSuffix(trStr, ".#") {
   790  			// Terraform 0.11 would often not validate traversals given in
   791  			// strings and so users would get away with this sort of
   792  			// flatmap-implementation-detail reference, particularly inside
   793  			// ignore_changes. We'll just trim these off to tolerate it,
   794  			// rather than failing below in ParseTraversalAbs.
   795  			trStr = trStr[:len(trStr)-2]
   796  		}
   797  		trSrc := []byte(trStr)
   798  		_, trDiags := hcl2syntax.ParseTraversalAbs(trSrc, "", hcl2.Pos{})
   799  		if !trDiags.HasErrors() {
   800  			return trSrc, nil
   801  		}
   802  	}
   803  	return upgradeExpr(val, filename, false, an)
   804  }
   805  
   806  var hilArithmeticOpSyms = map[hilast.ArithmeticOp]string{
   807  	hilast.ArithmeticOpAdd: " + ",
   808  	hilast.ArithmeticOpSub: " - ",
   809  	hilast.ArithmeticOpMul: " * ",
   810  	hilast.ArithmeticOpDiv: " / ",
   811  	hilast.ArithmeticOpMod: " % ",
   812  
   813  	hilast.ArithmeticOpLogicalAnd: " && ",
   814  	hilast.ArithmeticOpLogicalOr:  " || ",
   815  
   816  	hilast.ArithmeticOpEqual:              " == ",
   817  	hilast.ArithmeticOpNotEqual:           " != ",
   818  	hilast.ArithmeticOpLessThan:           " < ",
   819  	hilast.ArithmeticOpLessThanOrEqual:    " <= ",
   820  	hilast.ArithmeticOpGreaterThan:        " > ",
   821  	hilast.ArithmeticOpGreaterThanOrEqual: " >= ",
   822  }
   823  
   824  // upgradeTraversalParts might alter the given split parts from a HIL-style
   825  // variable access to account for renamings made in Terraform v0.12.
   826  func upgradeTraversalParts(parts []string, an *analysis) []string {
   827  	parts = upgradeCountTraversalParts(parts, an)
   828  	parts = upgradeTerraformRemoteStateTraversalParts(parts, an)
   829  	return parts
   830  }
   831  
   832  func upgradeCountTraversalParts(parts []string, an *analysis) []string {
   833  	// test_instance.foo.id needs to become test_instance.foo[0].id if
   834  	// count is set for test_instance.foo. Likewise, if count _isn't_ set
   835  	// then test_instance.foo.0.id must become test_instance.foo.id.
   836  	if len(parts) < 3 {
   837  		return parts
   838  	}
   839  	var addr addrs.Resource
   840  	var idxIdx int
   841  	switch parts[0] {
   842  	case "data":
   843  		addr.Mode = addrs.DataResourceMode
   844  		addr.Type = parts[1]
   845  		addr.Name = parts[2]
   846  		idxIdx = 3
   847  	default:
   848  		addr.Mode = addrs.ManagedResourceMode
   849  		addr.Type = parts[0]
   850  		addr.Name = parts[1]
   851  		idxIdx = 2
   852  	}
   853  
   854  	hasCount, exists := an.ResourceHasCount[addr]
   855  	if !exists {
   856  		// Probably not actually a resource instance at all, then.
   857  		return parts
   858  	}
   859  
   860  	// Since at least one attribute is required after a resource reference
   861  	// prior to Terraform v0.12, we can assume there will be at least enough
   862  	// parts to contain the index even if no index is actually present.
   863  	if idxIdx >= len(parts) {
   864  		return parts
   865  	}
   866  
   867  	maybeIdx := parts[idxIdx]
   868  	switch {
   869  	case hasCount:
   870  		if _, err := strconv.Atoi(maybeIdx); err == nil || maybeIdx == "*" {
   871  			// Has an index already, so no changes required.
   872  			return parts
   873  		}
   874  		// Need to insert index zero at idxIdx.
   875  		log.Printf("[TRACE] configupgrade: %s has count but reference does not have index, so adding one", addr)
   876  		newParts := make([]string, len(parts)+1)
   877  		copy(newParts, parts[:idxIdx])
   878  		newParts[idxIdx] = "0"
   879  		copy(newParts[idxIdx+1:], parts[idxIdx:])
   880  		return newParts
   881  	default:
   882  		// For removing indexes we'll be more conservative and only remove
   883  		// exactly index "0", because other indexes on a resource without
   884  		// count are invalid anyway and we're better off letting the normal
   885  		// configuration parser deal with that.
   886  		if maybeIdx != "0" {
   887  			return parts
   888  		}
   889  
   890  		// Need to remove the index zero.
   891  		log.Printf("[TRACE] configupgrade: %s does not have count but reference has index, so removing it", addr)
   892  		newParts := make([]string, len(parts)-1)
   893  		copy(newParts, parts[:idxIdx])
   894  		copy(newParts[idxIdx:], parts[idxIdx+1:])
   895  		return newParts
   896  	}
   897  }
   898  
   899  func upgradeTerraformRemoteStateTraversalParts(parts []string, an *analysis) []string {
   900  	// data.terraform_remote_state.x.foo needs to become
   901  	// data.terraform_remote_state.x.outputs.foo unless "foo" is a real
   902  	// attribute in the object type implied by the remote state schema.
   903  	if len(parts) < 4 {
   904  		return parts
   905  	}
   906  	if parts[0] != "data" || parts[1] != "terraform_remote_state" {
   907  		return parts
   908  	}
   909  
   910  	attrIdx := 3
   911  	if parts[attrIdx] == "*" {
   912  		attrIdx = 4 // data.terraform_remote_state.x.*.foo
   913  	} else if _, err := strconv.Atoi(parts[attrIdx]); err == nil {
   914  		attrIdx = 4 // data.terraform_remote_state.x.1.foo
   915  	}
   916  	if attrIdx >= len(parts) {
   917  		return parts
   918  	}
   919  
   920  	attrName := parts[attrIdx]
   921  
   922  	// Now we'll use the schema of data.terraform_remote_state to decide if
   923  	// the user intended this to be an output, or whether it's one of the real
   924  	// attributes of this data source.
   925  	var schema *configschema.Block
   926  	if providerSchema := an.ProviderSchemas["terraform"]; providerSchema != nil {
   927  		schema, _ = providerSchema.SchemaForResourceType(addrs.DataResourceMode, "terraform_remote_state")
   928  	}
   929  	// Schema should be available in all reasonable cases, but might be nil
   930  	// if input configuration contains a reference to a remote state data resource
   931  	// without actually defining that data resource. In that weird edge case,
   932  	// we'll just assume all attributes are outputs.
   933  	if schema != nil && schema.ImpliedType().HasAttribute(attrName) {
   934  		// User is accessing one of the real attributes, then, and we have
   935  		// no need to rewrite it.
   936  		return parts
   937  	}
   938  
   939  	// If we get down here then our task is to produce a new parts slice
   940  	// that has the fixed additional attribute name "outputs" inserted at
   941  	// attrIdx, retaining all other parts.
   942  	newParts := make([]string, len(parts)+1)
   943  	copy(newParts, parts[:attrIdx])
   944  	newParts[attrIdx] = "outputs"
   945  	copy(newParts[attrIdx+1:], parts[attrIdx:])
   946  	return newParts
   947  }
   948  
   949  func typeIsSettableFromTupleCons(ty cty.Type) bool {
   950  	return ty.IsListType() || ty.IsTupleType() || ty.IsSetType()
   951  }