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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package hcl
     5  
     6  import (
     7  	"fmt"
     8  	"math/big"
     9  
    10  	"github.com/zclconf/go-cty/cty"
    11  	"github.com/zclconf/go-cty/cty/convert"
    12  )
    13  
    14  // Index is a helper function that performs the same operation as the index
    15  // operator in the HCL expression language. That is, the result is the
    16  // same as it would be for collection[key] in a configuration expression.
    17  //
    18  // This is exported so that applications can perform indexing in a manner
    19  // consistent with how the language does it, including handling of null and
    20  // unknown values, etc.
    21  //
    22  // Diagnostics are produced if the given combination of values is not valid.
    23  // Therefore a pointer to a source range must be provided to use in diagnostics,
    24  // though nil can be provided if the calling application is going to
    25  // ignore the subject of the returned diagnostics anyway.
    26  func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics) {
    27  	const invalidIndex = "Invalid index"
    28  
    29  	if collection.IsNull() {
    30  		return cty.DynamicVal, Diagnostics{
    31  			{
    32  				Severity: DiagError,
    33  				Summary:  "Attempt to index null value",
    34  				Detail:   "This value is null, so it does not have any indices.",
    35  				Subject:  srcRange,
    36  			},
    37  		}
    38  	}
    39  	if key.IsNull() {
    40  		return cty.DynamicVal, Diagnostics{
    41  			{
    42  				Severity: DiagError,
    43  				Summary:  invalidIndex,
    44  				Detail:   "Can't use a null value as an indexing key.",
    45  				Subject:  srcRange,
    46  			},
    47  		}
    48  	}
    49  	ty := collection.Type()
    50  	kty := key.Type()
    51  	if kty == cty.DynamicPseudoType || ty == cty.DynamicPseudoType {
    52  		return cty.DynamicVal, nil
    53  	}
    54  
    55  	switch {
    56  
    57  	case ty.IsListType() || ty.IsTupleType() || ty.IsMapType():
    58  		var wantType cty.Type
    59  		switch {
    60  		case ty.IsListType() || ty.IsTupleType():
    61  			wantType = cty.Number
    62  		case ty.IsMapType():
    63  			wantType = cty.String
    64  		default:
    65  			// should never happen
    66  			panic("don't know what key type we want")
    67  		}
    68  
    69  		key, keyErr := convert.Convert(key, wantType)
    70  		if keyErr != nil {
    71  			return cty.DynamicVal, Diagnostics{
    72  				{
    73  					Severity: DiagError,
    74  					Summary:  invalidIndex,
    75  					Detail: fmt.Sprintf(
    76  						"The given key does not identify an element in this collection value: %s.",
    77  						keyErr.Error(),
    78  					),
    79  					Subject: srcRange,
    80  				},
    81  			}
    82  		}
    83  
    84  		// Here we drop marks from HasIndex result, in order to allow basic
    85  		// traversal of a marked list, tuple, or map in the same way we can
    86  		// traverse a marked object
    87  		has, _ := collection.HasIndex(key).Unmark()
    88  		if !has.IsKnown() {
    89  			if ty.IsTupleType() {
    90  				return cty.DynamicVal, nil
    91  			} else {
    92  				return cty.UnknownVal(ty.ElementType()), nil
    93  			}
    94  		}
    95  		if has.False() {
    96  			if (ty.IsListType() || ty.IsTupleType()) && key.Type().Equals(cty.Number) {
    97  				if key.IsKnown() && !key.IsNull() {
    98  					// NOTE: we don't know what any marks might've represented
    99  					// up at the calling application layer, so we must avoid
   100  					// showing the literal number value in these error messages
   101  					// in case the mark represents something important, such as
   102  					// a value being "sensitive".
   103  					key, _ := key.Unmark()
   104  					bf := key.AsBigFloat()
   105  					if _, acc := bf.Int(nil); acc != big.Exact {
   106  						// We have a more specialized error message for the
   107  						// situation of using a fractional number to index into
   108  						// a sequence, because that will tend to happen if the
   109  						// user is trying to use division to calculate an index
   110  						// and not realizing that HCL does float division
   111  						// rather than integer division.
   112  						return cty.DynamicVal, Diagnostics{
   113  							{
   114  								Severity: DiagError,
   115  								Summary:  invalidIndex,
   116  								Detail:   "The given key does not identify an element in this collection value: indexing a sequence requires a whole number, but the given index has a fractional part.",
   117  								Subject:  srcRange,
   118  							},
   119  						}
   120  					}
   121  
   122  					if bf.Sign() < 0 {
   123  						// Some other languages allow negative indices to
   124  						// select "backwards" from the end of the sequence,
   125  						// but HCL doesn't do that in order to give better
   126  						// feedback if a dynamic index is calculated
   127  						// incorrectly.
   128  						return cty.DynamicVal, Diagnostics{
   129  							{
   130  								Severity: DiagError,
   131  								Summary:  invalidIndex,
   132  								Detail:   "The given key does not identify an element in this collection value: a negative number is not a valid index for a sequence.",
   133  								Subject:  srcRange,
   134  							},
   135  						}
   136  					}
   137  					if lenVal := collection.Length(); lenVal.IsKnown() && !lenVal.IsMarked() {
   138  						// Length always returns a number, and we already
   139  						// checked that it's a known number, so this is safe.
   140  						lenBF := lenVal.AsBigFloat()
   141  						var result big.Float
   142  						result.Sub(bf, lenBF)
   143  						if result.Sign() < 1 {
   144  							if lenBF.Sign() == 0 {
   145  								return cty.DynamicVal, Diagnostics{
   146  									{
   147  										Severity: DiagError,
   148  										Summary:  invalidIndex,
   149  										Detail:   "The given key does not identify an element in this collection value: the collection has no elements.",
   150  										Subject:  srcRange,
   151  									},
   152  								}
   153  							} else {
   154  								return cty.DynamicVal, Diagnostics{
   155  									{
   156  										Severity: DiagError,
   157  										Summary:  invalidIndex,
   158  										Detail:   "The given key does not identify an element in this collection value: the given index is greater than or equal to the length of the collection.",
   159  										Subject:  srcRange,
   160  									},
   161  								}
   162  							}
   163  						}
   164  					}
   165  				}
   166  			}
   167  
   168  			// If this is not one of the special situations we handled above
   169  			// then we'll fall back on a very generic message.
   170  			return cty.DynamicVal, Diagnostics{
   171  				{
   172  					Severity: DiagError,
   173  					Summary:  invalidIndex,
   174  					Detail:   "The given key does not identify an element in this collection value.",
   175  					Subject:  srcRange,
   176  				},
   177  			}
   178  		}
   179  
   180  		return collection.Index(key), nil
   181  
   182  	case ty.IsObjectType():
   183  		wasNumber := key.Type() == cty.Number
   184  		key, keyErr := convert.Convert(key, cty.String)
   185  		if keyErr != nil {
   186  			return cty.DynamicVal, Diagnostics{
   187  				{
   188  					Severity: DiagError,
   189  					Summary:  invalidIndex,
   190  					Detail: fmt.Sprintf(
   191  						"The given key does not identify an element in this collection value: %s.",
   192  						keyErr.Error(),
   193  					),
   194  					Subject: srcRange,
   195  				},
   196  			}
   197  		}
   198  		if !collection.IsKnown() {
   199  			return cty.DynamicVal, nil
   200  		}
   201  		if !key.IsKnown() {
   202  			return cty.DynamicVal, nil
   203  		}
   204  
   205  		key, _ = key.Unmark()
   206  		attrName := key.AsString()
   207  
   208  		if !ty.HasAttribute(attrName) {
   209  			var suggestion string
   210  			if wasNumber {
   211  				// We note this only as an addendum to an error we would've
   212  				// already returned anyway, because it is valid (albeit weird)
   213  				// to have an attribute whose name is just decimal digits
   214  				// and then access that attribute using a number whose
   215  				// decimal representation is the same digits.
   216  				suggestion = " An object only supports looking up attributes by name, not by numeric index."
   217  			}
   218  			return cty.DynamicVal, Diagnostics{
   219  				{
   220  					Severity: DiagError,
   221  					Summary:  invalidIndex,
   222  					Detail:   fmt.Sprintf("The given key does not identify an element in this collection value.%s", suggestion),
   223  					Subject:  srcRange,
   224  				},
   225  			}
   226  		}
   227  
   228  		return collection.GetAttr(attrName), nil
   229  
   230  	case ty.IsSetType():
   231  		return cty.DynamicVal, Diagnostics{
   232  			{
   233  				Severity: DiagError,
   234  				Summary:  invalidIndex,
   235  				Detail:   "Elements of a set are identified only by their value and don't have any separate index or key to select with, so it's only possible to perform operations across all elements of the set.",
   236  				Subject:  srcRange,
   237  			},
   238  		}
   239  
   240  	default:
   241  		return cty.DynamicVal, Diagnostics{
   242  			{
   243  				Severity: DiagError,
   244  				Summary:  invalidIndex,
   245  				Detail:   "This value does not have any indices.",
   246  				Subject:  srcRange,
   247  			},
   248  		}
   249  	}
   250  
   251  }
   252  
   253  // GetAttr is a helper function that performs the same operation as the
   254  // attribute access in the HCL expression language. That is, the result is the
   255  // same as it would be for obj.attr in a configuration expression.
   256  //
   257  // This is exported so that applications can access attributes in a manner
   258  // consistent with how the language does it, including handling of null and
   259  // unknown values, etc.
   260  //
   261  // Diagnostics are produced if the given combination of values is not valid.
   262  // Therefore a pointer to a source range must be provided to use in diagnostics,
   263  // though nil can be provided if the calling application is going to
   264  // ignore the subject of the returned diagnostics anyway.
   265  func GetAttr(obj cty.Value, attrName string, srcRange *Range) (cty.Value, Diagnostics) {
   266  	if obj.IsNull() {
   267  		return cty.DynamicVal, Diagnostics{
   268  			{
   269  				Severity: DiagError,
   270  				Summary:  "Attempt to get attribute from null value",
   271  				Detail:   "This value is null, so it does not have any attributes.",
   272  				Subject:  srcRange,
   273  			},
   274  		}
   275  	}
   276  
   277  	const unsupportedAttr = "Unsupported attribute"
   278  
   279  	ty := obj.Type()
   280  	switch {
   281  	case ty.IsObjectType():
   282  		if !ty.HasAttribute(attrName) {
   283  			return cty.DynamicVal, Diagnostics{
   284  				{
   285  					Severity: DiagError,
   286  					Summary:  unsupportedAttr,
   287  					Detail:   fmt.Sprintf("This object does not have an attribute named %q.", attrName),
   288  					Subject:  srcRange,
   289  				},
   290  			}
   291  		}
   292  
   293  		if !obj.IsKnown() {
   294  			return cty.UnknownVal(ty.AttributeType(attrName)), nil
   295  		}
   296  
   297  		return obj.GetAttr(attrName), nil
   298  	case ty.IsMapType():
   299  		if !obj.IsKnown() {
   300  			return cty.UnknownVal(ty.ElementType()), nil
   301  		}
   302  
   303  		idx := cty.StringVal(attrName)
   304  
   305  		// Here we drop marks from HasIndex result, in order to allow basic
   306  		// traversal of a marked map in the same way we can traverse a marked
   307  		// object
   308  		hasIndex, _ := obj.HasIndex(idx).Unmark()
   309  		if hasIndex.False() {
   310  			return cty.DynamicVal, Diagnostics{
   311  				{
   312  					Severity: DiagError,
   313  					Summary:  "Missing map element",
   314  					Detail:   fmt.Sprintf("This map does not have an element with the key %q.", attrName),
   315  					Subject:  srcRange,
   316  				},
   317  			}
   318  		}
   319  
   320  		return obj.Index(idx), nil
   321  	case ty == cty.DynamicPseudoType:
   322  		return cty.DynamicVal, nil
   323  	case ty.IsListType() && ty.ElementType().IsObjectType():
   324  		// It seems a common mistake to try to access attributes on a whole
   325  		// list of objects rather than on a specific individual element, so
   326  		// we have some extra hints for that case.
   327  
   328  		switch {
   329  		case ty.ElementType().HasAttribute(attrName):
   330  			// This is a very strong indication that the user mistook the list
   331  			// of objects for a single object, so we can be a little more
   332  			// direct in our suggestion here.
   333  			return cty.DynamicVal, Diagnostics{
   334  				{
   335  					Severity: DiagError,
   336  					Summary:  unsupportedAttr,
   337  					Detail:   fmt.Sprintf("Can't access attributes on a list of objects. Did you mean to access attribute %q for a specific element of the list, or across all elements of the list?", attrName),
   338  					Subject:  srcRange,
   339  				},
   340  			}
   341  		default:
   342  			return cty.DynamicVal, Diagnostics{
   343  				{
   344  					Severity: DiagError,
   345  					Summary:  unsupportedAttr,
   346  					Detail:   "Can't access attributes on a list of objects. Did you mean to access an attribute for a specific element of the list, or across all elements of the list?",
   347  					Subject:  srcRange,
   348  				},
   349  			}
   350  		}
   351  
   352  	case ty.IsSetType() && ty.ElementType().IsObjectType():
   353  		// This is similar to the previous case, but we can't give such a
   354  		// direct suggestion because there is no mechanism to select a single
   355  		// item from a set.
   356  		// We could potentially suggest using a for expression or splat
   357  		// operator here, but we typically don't get into syntax specifics
   358  		// in hcl.GetAttr suggestions because it's a general function used in
   359  		// various other situations, such as in application-specific operations
   360  		// that might have a more constraint set of alternative approaches.
   361  
   362  		return cty.DynamicVal, Diagnostics{
   363  			{
   364  				Severity: DiagError,
   365  				Summary:  unsupportedAttr,
   366  				Detail:   "Can't access attributes on a set of objects. Did you mean to access an attribute across all elements of the set?",
   367  				Subject:  srcRange,
   368  			},
   369  		}
   370  
   371  	case ty.IsPrimitiveType():
   372  		return cty.DynamicVal, Diagnostics{
   373  			{
   374  				Severity: DiagError,
   375  				Summary:  unsupportedAttr,
   376  				Detail:   fmt.Sprintf("Can't access attributes on a primitive-typed value (%s).", ty.FriendlyName()),
   377  				Subject:  srcRange,
   378  			},
   379  		}
   380  
   381  	default:
   382  		return cty.DynamicVal, Diagnostics{
   383  			{
   384  				Severity: DiagError,
   385  				Summary:  unsupportedAttr,
   386  				Detail:   "This value does not have any attributes.",
   387  				Subject:  srcRange,
   388  			},
   389  		}
   390  	}
   391  
   392  }
   393  
   394  // ApplyPath is a helper function that applies a cty.Path to a value using the
   395  // indexing and attribute access operations from HCL.
   396  //
   397  // This is similar to calling the path's own Apply method, but ApplyPath uses
   398  // the more relaxed typing rules that apply to these operations in HCL, rather
   399  // than cty's relatively-strict rules. ApplyPath is implemented in terms of
   400  // Index and GetAttr, and so it has the same behavior for individual steps
   401  // but will stop and return any errors returned by intermediate steps.
   402  //
   403  // Diagnostics are produced if the given path cannot be applied to the given
   404  // value. Therefore a pointer to a source range must be provided to use in
   405  // diagnostics, though nil can be provided if the calling application is going
   406  // to ignore the subject of the returned diagnostics anyway.
   407  func ApplyPath(val cty.Value, path cty.Path, srcRange *Range) (cty.Value, Diagnostics) {
   408  	var diags Diagnostics
   409  
   410  	for _, step := range path {
   411  		var stepDiags Diagnostics
   412  		switch ts := step.(type) {
   413  		case cty.IndexStep:
   414  			val, stepDiags = Index(val, ts.Key, srcRange)
   415  		case cty.GetAttrStep:
   416  			val, stepDiags = GetAttr(val, ts.Name, srcRange)
   417  		default:
   418  			// Should never happen because the above are all of the step types.
   419  			diags = diags.Append(&Diagnostic{
   420  				Severity: DiagError,
   421  				Summary:  "Invalid path step",
   422  				Detail:   fmt.Sprintf("Go type %T is not a valid path step. This is a bug in this program.", step),
   423  				Subject:  srcRange,
   424  			})
   425  			return cty.DynamicVal, diags
   426  		}
   427  
   428  		diags = append(diags, stepDiags...)
   429  		if stepDiags.HasErrors() {
   430  			return cty.DynamicVal, diags
   431  		}
   432  	}
   433  
   434  	return val, diags
   435  }