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 }