github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/addrs/parse_ref.go (about) 1 package addrs 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/hashicorp/hcl/v2" 8 "github.com/hashicorp/hcl/v2/hclsyntax" 9 "github.com/eliastor/durgaform/internal/tfdiags" 10 "github.com/zclconf/go-cty/cty" 11 ) 12 13 // Reference describes a reference to an address with source location 14 // information. 15 type Reference struct { 16 Subject Referenceable 17 SourceRange tfdiags.SourceRange 18 Remaining hcl.Traversal 19 } 20 21 // DisplayString returns a string that approximates the subject and remaining 22 // traversal of the reciever in a way that resembles the Durgaform language 23 // syntax that could've produced it. 24 // 25 // It's not guaranteed to actually be a valid Durgaform language expression, 26 // since the intended use here is primarily for UI messages such as 27 // diagnostics. 28 func (r *Reference) DisplayString() string { 29 if len(r.Remaining) == 0 { 30 // Easy case: we can just return the subject's string. 31 return r.Subject.String() 32 } 33 34 var ret strings.Builder 35 ret.WriteString(r.Subject.String()) 36 for _, step := range r.Remaining { 37 switch tStep := step.(type) { 38 case hcl.TraverseRoot: 39 ret.WriteString(tStep.Name) 40 case hcl.TraverseAttr: 41 ret.WriteByte('.') 42 ret.WriteString(tStep.Name) 43 case hcl.TraverseIndex: 44 ret.WriteByte('[') 45 switch tStep.Key.Type() { 46 case cty.String: 47 ret.WriteString(fmt.Sprintf("%q", tStep.Key.AsString())) 48 case cty.Number: 49 bf := tStep.Key.AsBigFloat() 50 ret.WriteString(bf.Text('g', 10)) 51 } 52 ret.WriteByte(']') 53 } 54 } 55 return ret.String() 56 } 57 58 // ParseRef attempts to extract a referencable address from the prefix of the 59 // given traversal, which must be an absolute traversal or this function 60 // will panic. 61 // 62 // If no error diagnostics are returned, the returned reference includes the 63 // address that was extracted, the source range it was extracted from, and any 64 // remaining relative traversal that was not consumed as part of the 65 // reference. 66 // 67 // If error diagnostics are returned then the Reference value is invalid and 68 // must not be used. 69 func ParseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 70 ref, diags := parseRef(traversal) 71 72 // Normalize a little to make life easier for callers. 73 if ref != nil { 74 if len(ref.Remaining) == 0 { 75 ref.Remaining = nil 76 } 77 } 78 79 return ref, diags 80 } 81 82 // ParseRefStr is a helper wrapper around ParseRef that takes a string 83 // and parses it with the HCL native syntax traversal parser before 84 // interpreting it. 85 // 86 // This should be used only in specialized situations since it will cause the 87 // created references to not have any meaningful source location information. 88 // If a reference string is coming from a source that should be identified in 89 // error messages then the caller should instead parse it directly using a 90 // suitable function from the HCL API and pass the traversal itself to 91 // ParseRef. 92 // 93 // Error diagnostics are returned if either the parsing fails or the analysis 94 // of the traversal fails. There is no way for the caller to distinguish the 95 // two kinds of diagnostics programmatically. If error diagnostics are returned 96 // the returned reference may be nil or incomplete. 97 func ParseRefStr(str string) (*Reference, tfdiags.Diagnostics) { 98 var diags tfdiags.Diagnostics 99 100 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 101 diags = diags.Append(parseDiags) 102 if parseDiags.HasErrors() { 103 return nil, diags 104 } 105 106 ref, targetDiags := ParseRef(traversal) 107 diags = diags.Append(targetDiags) 108 return ref, diags 109 } 110 111 func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 112 var diags tfdiags.Diagnostics 113 114 root := traversal.RootName() 115 rootRange := traversal[0].SourceRange() 116 117 switch root { 118 119 case "count": 120 name, rng, remain, diags := parseSingleAttrRef(traversal) 121 return &Reference{ 122 Subject: CountAttr{Name: name}, 123 SourceRange: tfdiags.SourceRangeFromHCL(rng), 124 Remaining: remain, 125 }, diags 126 127 case "each": 128 name, rng, remain, diags := parseSingleAttrRef(traversal) 129 return &Reference{ 130 Subject: ForEachAttr{Name: name}, 131 SourceRange: tfdiags.SourceRangeFromHCL(rng), 132 Remaining: remain, 133 }, diags 134 135 case "data": 136 if len(traversal) < 3 { 137 diags = diags.Append(&hcl.Diagnostic{ 138 Severity: hcl.DiagError, 139 Summary: "Invalid reference", 140 Detail: `The "data" object must be followed by two attribute names: the data source type and the resource name.`, 141 Subject: traversal.SourceRange().Ptr(), 142 }) 143 return nil, diags 144 } 145 remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser 146 return parseResourceRef(DataResourceMode, rootRange, remain) 147 148 case "resource": 149 // This is an alias for the normal case of just using a managed resource 150 // type as a top-level symbol, which will serve as an escape mechanism 151 // if a later edition of the Durgaform language introduces a new 152 // reference prefix that conflicts with a resource type name in an 153 // existing provider. In that case, the edition upgrade tool can 154 // rewrite foo.bar into resource.foo.bar to ensure that "foo" remains 155 // interpreted as a resource type name rather than as the new reserved 156 // word. 157 if len(traversal) < 3 { 158 diags = diags.Append(&hcl.Diagnostic{ 159 Severity: hcl.DiagError, 160 Summary: "Invalid reference", 161 Detail: `The "resource" object must be followed by two attribute names: the resource type and the resource name.`, 162 Subject: traversal.SourceRange().Ptr(), 163 }) 164 return nil, diags 165 } 166 remain := traversal[1:] // trim off "resource" so we can use our shared resource reference parser 167 return parseResourceRef(ManagedResourceMode, rootRange, remain) 168 169 case "local": 170 name, rng, remain, diags := parseSingleAttrRef(traversal) 171 return &Reference{ 172 Subject: LocalValue{Name: name}, 173 SourceRange: tfdiags.SourceRangeFromHCL(rng), 174 Remaining: remain, 175 }, diags 176 177 case "module": 178 callName, callRange, remain, diags := parseSingleAttrRef(traversal) 179 if diags.HasErrors() { 180 return nil, diags 181 } 182 183 // A traversal starting with "module" can either be a reference to an 184 // entire module, or to a single output from a module instance, 185 // depending on what we find after this introducer. 186 callInstance := ModuleCallInstance{ 187 Call: ModuleCall{ 188 Name: callName, 189 }, 190 Key: NoKey, 191 } 192 193 if len(remain) == 0 { 194 // Reference to an entire module. Might alternatively be a 195 // reference to a single instance of a particular module, but the 196 // caller will need to deal with that ambiguity since we don't have 197 // enough context here. 198 return &Reference{ 199 Subject: callInstance.Call, 200 SourceRange: tfdiags.SourceRangeFromHCL(callRange), 201 Remaining: remain, 202 }, diags 203 } 204 205 if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 206 var err error 207 callInstance.Key, err = ParseInstanceKey(idxTrav.Key) 208 if err != nil { 209 diags = diags.Append(&hcl.Diagnostic{ 210 Severity: hcl.DiagError, 211 Summary: "Invalid index key", 212 Detail: fmt.Sprintf("Invalid index for module instance: %s.", err), 213 Subject: &idxTrav.SrcRange, 214 }) 215 return nil, diags 216 } 217 remain = remain[1:] 218 219 if len(remain) == 0 { 220 // Also a reference to an entire module instance, but we have a key 221 // now. 222 return &Reference{ 223 Subject: callInstance, 224 SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, idxTrav.SrcRange)), 225 Remaining: remain, 226 }, diags 227 } 228 } 229 230 if attrTrav, ok := remain[0].(hcl.TraverseAttr); ok { 231 remain = remain[1:] 232 return &Reference{ 233 Subject: ModuleCallInstanceOutput{ 234 Name: attrTrav.Name, 235 Call: callInstance, 236 }, 237 SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, attrTrav.SrcRange)), 238 Remaining: remain, 239 }, diags 240 } 241 242 diags = diags.Append(&hcl.Diagnostic{ 243 Severity: hcl.DiagError, 244 Summary: "Invalid reference", 245 Detail: "Module instance objects do not support this operation.", 246 Subject: remain[0].SourceRange().Ptr(), 247 }) 248 return nil, diags 249 250 case "path": 251 name, rng, remain, diags := parseSingleAttrRef(traversal) 252 return &Reference{ 253 Subject: PathAttr{Name: name}, 254 SourceRange: tfdiags.SourceRangeFromHCL(rng), 255 Remaining: remain, 256 }, diags 257 258 case "self": 259 return &Reference{ 260 Subject: Self, 261 SourceRange: tfdiags.SourceRangeFromHCL(rootRange), 262 Remaining: traversal[1:], 263 }, diags 264 265 case "durgaform": 266 name, rng, remain, diags := parseSingleAttrRef(traversal) 267 return &Reference{ 268 Subject: DurgaformAttr{Name: name}, 269 SourceRange: tfdiags.SourceRangeFromHCL(rng), 270 Remaining: remain, 271 }, diags 272 273 case "var": 274 name, rng, remain, diags := parseSingleAttrRef(traversal) 275 return &Reference{ 276 Subject: InputVariable{Name: name}, 277 SourceRange: tfdiags.SourceRangeFromHCL(rng), 278 Remaining: remain, 279 }, diags 280 281 case "template", "lazy", "arg": 282 // These names are all pre-emptively reserved in the hope of landing 283 // some version of "template values" or "lazy expressions" feature 284 // before the next opt-in language edition, but don't yet do anything. 285 diags = diags.Append(&hcl.Diagnostic{ 286 Severity: hcl.DiagError, 287 Summary: "Reserved symbol name", 288 Detail: fmt.Sprintf("The symbol name %q is reserved for use in a future Durgaform version. If you are using a provider that already uses this as a resource type name, add the prefix \"resource.\" to force interpretation as a resource type name.", root), 289 Subject: rootRange.Ptr(), 290 }) 291 return nil, diags 292 293 default: 294 return parseResourceRef(ManagedResourceMode, rootRange, traversal) 295 } 296 } 297 298 func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 299 var diags tfdiags.Diagnostics 300 301 if len(traversal) < 2 { 302 diags = diags.Append(&hcl.Diagnostic{ 303 Severity: hcl.DiagError, 304 Summary: "Invalid reference", 305 Detail: `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, 306 Subject: hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(), 307 }) 308 return nil, diags 309 } 310 311 var typeName, name string 312 switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode 313 case hcl.TraverseRoot: 314 typeName = tt.Name 315 case hcl.TraverseAttr: 316 typeName = tt.Name 317 default: 318 // If it isn't a TraverseRoot then it must be a "data" reference. 319 diags = diags.Append(&hcl.Diagnostic{ 320 Severity: hcl.DiagError, 321 Summary: "Invalid reference", 322 Detail: `The "data" object does not support this operation.`, 323 Subject: traversal[0].SourceRange().Ptr(), 324 }) 325 return nil, diags 326 } 327 328 attrTrav, ok := traversal[1].(hcl.TraverseAttr) 329 if !ok { 330 var what string 331 switch mode { 332 case DataResourceMode: 333 what = "data source" 334 default: 335 what = "resource type" 336 } 337 diags = diags.Append(&hcl.Diagnostic{ 338 Severity: hcl.DiagError, 339 Summary: "Invalid reference", 340 Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what), 341 Subject: traversal[1].SourceRange().Ptr(), 342 }) 343 return nil, diags 344 } 345 name = attrTrav.Name 346 rng := hcl.RangeBetween(startRange, attrTrav.SrcRange) 347 remain := traversal[2:] 348 349 resourceAddr := Resource{ 350 Mode: mode, 351 Type: typeName, 352 Name: name, 353 } 354 resourceInstAddr := ResourceInstance{ 355 Resource: resourceAddr, 356 Key: NoKey, 357 } 358 359 if len(remain) == 0 { 360 // This might actually be a reference to the collection of all instances 361 // of the resource, but we don't have enough context here to decide 362 // so we'll let the caller resolve that ambiguity. 363 return &Reference{ 364 Subject: resourceAddr, 365 SourceRange: tfdiags.SourceRangeFromHCL(rng), 366 }, diags 367 } 368 369 if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 370 var err error 371 resourceInstAddr.Key, err = ParseInstanceKey(idxTrav.Key) 372 if err != nil { 373 diags = diags.Append(&hcl.Diagnostic{ 374 Severity: hcl.DiagError, 375 Summary: "Invalid index key", 376 Detail: fmt.Sprintf("Invalid index for resource instance: %s.", err), 377 Subject: &idxTrav.SrcRange, 378 }) 379 return nil, diags 380 } 381 remain = remain[1:] 382 rng = hcl.RangeBetween(rng, idxTrav.SrcRange) 383 } 384 385 return &Reference{ 386 Subject: resourceInstAddr, 387 SourceRange: tfdiags.SourceRangeFromHCL(rng), 388 Remaining: remain, 389 }, diags 390 } 391 392 func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) { 393 var diags tfdiags.Diagnostics 394 395 root := traversal.RootName() 396 rootRange := traversal[0].SourceRange() 397 398 if len(traversal) < 2 { 399 diags = diags.Append(&hcl.Diagnostic{ 400 Severity: hcl.DiagError, 401 Summary: "Invalid reference", 402 Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root), 403 Subject: &rootRange, 404 }) 405 return "", hcl.Range{}, nil, diags 406 } 407 if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok { 408 return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags 409 } 410 diags = diags.Append(&hcl.Diagnostic{ 411 Severity: hcl.DiagError, 412 Summary: "Invalid reference", 413 Detail: fmt.Sprintf("The %q object does not support this operation.", root), 414 Subject: traversal[1].SourceRange().Ptr(), 415 }) 416 return "", hcl.Range{}, nil, diags 417 }