github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/terraform/addrs/parse_ref_test.go (about) 1 package addrs 2 3 import ( 4 "testing" 5 6 "github.com/go-test/deep" 7 "github.com/hashicorp/hcl/v2" 8 "github.com/hashicorp/hcl/v2/hclsyntax" 9 "github.com/zclconf/go-cty/cty" 10 ) 11 12 func TestParseRef(t *testing.T) { 13 tests := []struct { 14 Input string 15 Want *Reference 16 WantErr string 17 }{ 18 19 // count 20 { 21 `count.index`, 22 &Reference{ 23 Subject: CountAttr{ 24 Name: "index", 25 }, 26 SourceRange: hcl.Range{ 27 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 28 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 29 }, 30 }, 31 ``, 32 }, 33 { 34 `count.index.blah`, 35 &Reference{ 36 Subject: CountAttr{ 37 Name: "index", 38 }, 39 SourceRange: hcl.Range{ 40 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 41 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 42 }, 43 Remaining: hcl.Traversal{ 44 hcl.TraverseAttr{ 45 Name: "blah", 46 SrcRange: hcl.Range{ 47 Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 48 End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, 49 }, 50 }, 51 }, 52 }, 53 ``, // valid at this layer, but will fail during eval because "index" is a number 54 }, 55 { 56 `count`, 57 nil, 58 `The "count" object cannot be accessed directly. Instead, access one of its attributes.`, 59 }, 60 { 61 `count["hello"]`, 62 nil, 63 `The "count" object does not support this operation.`, 64 }, 65 66 // each 67 { 68 `each.key`, 69 &Reference{ 70 Subject: ForEachAttr{ 71 Name: "key", 72 }, 73 SourceRange: hcl.Range{ 74 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 75 End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, 76 }, 77 }, 78 ``, 79 }, 80 { 81 `each.value.blah`, 82 &Reference{ 83 Subject: ForEachAttr{ 84 Name: "value", 85 }, 86 SourceRange: hcl.Range{ 87 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 88 End: hcl.Pos{Line: 1, Column: 11, Byte: 10}, 89 }, 90 Remaining: hcl.Traversal{ 91 hcl.TraverseAttr{ 92 Name: "blah", 93 SrcRange: hcl.Range{ 94 Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, 95 End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, 96 }, 97 }, 98 }, 99 }, 100 ``, 101 }, 102 { 103 `each`, 104 nil, 105 `The "each" object cannot be accessed directly. Instead, access one of its attributes.`, 106 }, 107 { 108 `each["hello"]`, 109 nil, 110 `The "each" object does not support this operation.`, 111 }, 112 // data 113 { 114 `data.external.foo`, 115 &Reference{ 116 Subject: Resource{ 117 Mode: DataResourceMode, 118 Type: "external", 119 Name: "foo", 120 }, 121 SourceRange: hcl.Range{ 122 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 123 End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 124 }, 125 }, 126 ``, 127 }, 128 { 129 `data.external.foo.bar`, 130 &Reference{ 131 Subject: ResourceInstance{ 132 Resource: Resource{ 133 Mode: DataResourceMode, 134 Type: "external", 135 Name: "foo", 136 }, 137 }, 138 SourceRange: hcl.Range{ 139 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 140 End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 141 }, 142 Remaining: hcl.Traversal{ 143 hcl.TraverseAttr{ 144 Name: "bar", 145 SrcRange: hcl.Range{ 146 Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 147 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 148 }, 149 }, 150 }, 151 }, 152 ``, 153 }, 154 { 155 `data.external.foo["baz"].bar`, 156 &Reference{ 157 Subject: ResourceInstance{ 158 Resource: Resource{ 159 Mode: DataResourceMode, 160 Type: "external", 161 Name: "foo", 162 }, 163 Key: StringKey("baz"), 164 }, 165 SourceRange: hcl.Range{ 166 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 167 End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 168 }, 169 Remaining: hcl.Traversal{ 170 hcl.TraverseAttr{ 171 Name: "bar", 172 SrcRange: hcl.Range{ 173 Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 174 End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, 175 }, 176 }, 177 }, 178 }, 179 ``, 180 }, 181 { 182 `data.external.foo["baz"]`, 183 &Reference{ 184 Subject: ResourceInstance{ 185 Resource: Resource{ 186 Mode: DataResourceMode, 187 Type: "external", 188 Name: "foo", 189 }, 190 Key: StringKey("baz"), 191 }, 192 SourceRange: hcl.Range{ 193 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 194 End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 195 }, 196 }, 197 ``, 198 }, 199 { 200 `data`, 201 nil, 202 `The "data" object must be followed by two attribute names: the data source type and the resource name.`, 203 }, 204 { 205 `data.external`, 206 nil, 207 `The "data" object must be followed by two attribute names: the data source type and the resource name.`, 208 }, 209 210 // local 211 { 212 `local.foo`, 213 &Reference{ 214 Subject: LocalValue{ 215 Name: "foo", 216 }, 217 SourceRange: hcl.Range{ 218 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 219 End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 220 }, 221 }, 222 ``, 223 }, 224 { 225 `local.foo.blah`, 226 &Reference{ 227 Subject: LocalValue{ 228 Name: "foo", 229 }, 230 SourceRange: hcl.Range{ 231 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 232 End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 233 }, 234 Remaining: hcl.Traversal{ 235 hcl.TraverseAttr{ 236 Name: "blah", 237 SrcRange: hcl.Range{ 238 Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 239 End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, 240 }, 241 }, 242 }, 243 }, 244 ``, 245 }, 246 { 247 `local.foo["blah"]`, 248 &Reference{ 249 Subject: LocalValue{ 250 Name: "foo", 251 }, 252 SourceRange: hcl.Range{ 253 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 254 End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 255 }, 256 Remaining: hcl.Traversal{ 257 hcl.TraverseIndex{ 258 Key: cty.StringVal("blah"), 259 SrcRange: hcl.Range{ 260 Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 261 End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 262 }, 263 }, 264 }, 265 }, 266 ``, 267 }, 268 { 269 `local`, 270 nil, 271 `The "local" object cannot be accessed directly. Instead, access one of its attributes.`, 272 }, 273 { 274 `local["foo"]`, 275 nil, 276 `The "local" object does not support this operation.`, 277 }, 278 279 // module 280 { 281 `module.foo`, 282 &Reference{ 283 Subject: ModuleCall{ 284 Name: "foo", 285 }, 286 SourceRange: hcl.Range{ 287 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 288 End: hcl.Pos{Line: 1, Column: 11, Byte: 10}, 289 }, 290 }, 291 ``, 292 }, 293 { 294 `module.foo.bar`, 295 &Reference{ 296 Subject: ModuleCallInstanceOutput{ 297 Call: ModuleCallInstance{ 298 Call: ModuleCall{ 299 Name: "foo", 300 }, 301 }, 302 Name: "bar", 303 }, 304 SourceRange: hcl.Range{ 305 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 306 End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, 307 }, 308 }, 309 ``, 310 }, 311 { 312 `module.foo.bar.baz`, 313 &Reference{ 314 Subject: ModuleCallInstanceOutput{ 315 Call: ModuleCallInstance{ 316 Call: ModuleCall{ 317 Name: "foo", 318 }, 319 }, 320 Name: "bar", 321 }, 322 SourceRange: hcl.Range{ 323 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 324 End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, 325 }, 326 Remaining: hcl.Traversal{ 327 hcl.TraverseAttr{ 328 Name: "baz", 329 SrcRange: hcl.Range{ 330 Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, 331 End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, 332 }, 333 }, 334 }, 335 }, 336 ``, 337 }, 338 { 339 `module.foo["baz"]`, 340 &Reference{ 341 Subject: ModuleCallInstance{ 342 Call: ModuleCall{ 343 Name: "foo", 344 }, 345 Key: StringKey("baz"), 346 }, 347 SourceRange: hcl.Range{ 348 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 349 End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 350 }, 351 }, 352 ``, 353 }, 354 { 355 `module.foo["baz"].bar`, 356 &Reference{ 357 Subject: ModuleCallInstanceOutput{ 358 Call: ModuleCallInstance{ 359 Call: ModuleCall{ 360 Name: "foo", 361 }, 362 Key: StringKey("baz"), 363 }, 364 Name: "bar", 365 }, 366 SourceRange: hcl.Range{ 367 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 368 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 369 }, 370 }, 371 ``, 372 }, 373 { 374 `module.foo["baz"].bar.boop`, 375 &Reference{ 376 Subject: ModuleCallInstanceOutput{ 377 Call: ModuleCallInstance{ 378 Call: ModuleCall{ 379 Name: "foo", 380 }, 381 Key: StringKey("baz"), 382 }, 383 Name: "bar", 384 }, 385 SourceRange: hcl.Range{ 386 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 387 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 388 }, 389 Remaining: hcl.Traversal{ 390 hcl.TraverseAttr{ 391 Name: "boop", 392 SrcRange: hcl.Range{ 393 Start: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 394 End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, 395 }, 396 }, 397 }, 398 }, 399 ``, 400 }, 401 { 402 `module`, 403 nil, 404 `The "module" object cannot be accessed directly. Instead, access one of its attributes.`, 405 }, 406 { 407 `module["foo"]`, 408 nil, 409 `The "module" object does not support this operation.`, 410 }, 411 412 // path 413 { 414 `path.module`, 415 &Reference{ 416 Subject: PathAttr{ 417 Name: "module", 418 }, 419 SourceRange: hcl.Range{ 420 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 421 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 422 }, 423 }, 424 ``, 425 }, 426 { 427 `path.module.blah`, 428 &Reference{ 429 Subject: PathAttr{ 430 Name: "module", 431 }, 432 SourceRange: hcl.Range{ 433 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 434 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 435 }, 436 Remaining: hcl.Traversal{ 437 hcl.TraverseAttr{ 438 Name: "blah", 439 SrcRange: hcl.Range{ 440 Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 441 End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, 442 }, 443 }, 444 }, 445 }, 446 ``, // valid at this layer, but will fail during eval because "module" is a string 447 }, 448 { 449 `path`, 450 nil, 451 `The "path" object cannot be accessed directly. Instead, access one of its attributes.`, 452 }, 453 { 454 `path["module"]`, 455 nil, 456 `The "path" object does not support this operation.`, 457 }, 458 459 // self 460 { 461 `self`, 462 &Reference{ 463 Subject: Self, 464 SourceRange: hcl.Range{ 465 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 466 End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, 467 }, 468 }, 469 ``, 470 }, 471 { 472 `self.blah`, 473 &Reference{ 474 Subject: Self, 475 SourceRange: hcl.Range{ 476 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 477 End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, 478 }, 479 Remaining: hcl.Traversal{ 480 hcl.TraverseAttr{ 481 Name: "blah", 482 SrcRange: hcl.Range{ 483 Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, 484 End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 485 }, 486 }, 487 }, 488 }, 489 ``, 490 }, 491 492 // terraform 493 { 494 `terraform.workspace`, 495 &Reference{ 496 Subject: TerraformAttr{ 497 Name: "workspace", 498 }, 499 SourceRange: hcl.Range{ 500 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 501 End: hcl.Pos{Line: 1, Column: 20, Byte: 19}, 502 }, 503 }, 504 ``, 505 }, 506 { 507 `terraform.workspace.blah`, 508 &Reference{ 509 Subject: TerraformAttr{ 510 Name: "workspace", 511 }, 512 SourceRange: hcl.Range{ 513 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 514 End: hcl.Pos{Line: 1, Column: 20, Byte: 19}, 515 }, 516 Remaining: hcl.Traversal{ 517 hcl.TraverseAttr{ 518 Name: "blah", 519 SrcRange: hcl.Range{ 520 Start: hcl.Pos{Line: 1, Column: 20, Byte: 19}, 521 End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 522 }, 523 }, 524 }, 525 }, 526 ``, // valid at this layer, but will fail during eval because "workspace" is a string 527 }, 528 { 529 `terraform`, 530 nil, 531 `The "terraform" object cannot be accessed directly. Instead, access one of its attributes.`, 532 }, 533 { 534 `terraform["workspace"]`, 535 nil, 536 `The "terraform" object does not support this operation.`, 537 }, 538 539 // var 540 { 541 `var.foo`, 542 &Reference{ 543 Subject: InputVariable{ 544 Name: "foo", 545 }, 546 SourceRange: hcl.Range{ 547 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 548 End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 549 }, 550 }, 551 ``, 552 }, 553 { 554 `var.foo.blah`, 555 &Reference{ 556 Subject: InputVariable{ 557 Name: "foo", 558 }, 559 SourceRange: hcl.Range{ 560 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 561 End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 562 }, 563 Remaining: hcl.Traversal{ 564 hcl.TraverseAttr{ 565 Name: "blah", 566 SrcRange: hcl.Range{ 567 Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 568 End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, 569 }, 570 }, 571 }, 572 }, 573 ``, // valid at this layer, but will fail during eval because "module" is a string 574 }, 575 { 576 `var`, 577 nil, 578 `The "var" object cannot be accessed directly. Instead, access one of its attributes.`, 579 }, 580 { 581 `var["foo"]`, 582 nil, 583 `The "var" object does not support this operation.`, 584 }, 585 586 // the "resource" prefix forces interpreting the next name as a 587 // resource type name. This is an alias for just using a resource 588 // type name at the top level, to be used only if a later edition 589 // of the Terraform language introduces a new reserved word that 590 // overlaps with a resource type name. 591 { 592 `resource.boop_instance.foo`, 593 &Reference{ 594 Subject: Resource{ 595 Mode: ManagedResourceMode, 596 Type: "boop_instance", 597 Name: "foo", 598 }, 599 SourceRange: hcl.Range{ 600 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 601 End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, 602 }, 603 }, 604 ``, 605 }, 606 607 // We have some names reserved which might be used by a 608 // still-under-discussion proposal for template values or lazy 609 // expressions. 610 { 611 `template.foo`, 612 nil, 613 `The symbol name "template" is reserved for use in a future Terraform 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.`, 614 }, 615 { 616 `lazy.foo`, 617 nil, 618 `The symbol name "lazy" is reserved for use in a future Terraform 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.`, 619 }, 620 { 621 `arg.foo`, 622 nil, 623 `The symbol name "arg" is reserved for use in a future Terraform 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.`, 624 }, 625 626 // anything else, interpreted as a managed resource reference 627 { 628 `boop_instance.foo`, 629 &Reference{ 630 Subject: Resource{ 631 Mode: ManagedResourceMode, 632 Type: "boop_instance", 633 Name: "foo", 634 }, 635 SourceRange: hcl.Range{ 636 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 637 End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 638 }, 639 }, 640 ``, 641 }, 642 { 643 `boop_instance.foo.bar`, 644 &Reference{ 645 Subject: ResourceInstance{ 646 Resource: Resource{ 647 Mode: ManagedResourceMode, 648 Type: "boop_instance", 649 Name: "foo", 650 }, 651 }, 652 SourceRange: hcl.Range{ 653 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 654 End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 655 }, 656 Remaining: hcl.Traversal{ 657 hcl.TraverseAttr{ 658 Name: "bar", 659 SrcRange: hcl.Range{ 660 Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 661 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 662 }, 663 }, 664 }, 665 }, 666 ``, 667 }, 668 { 669 `boop_instance.foo["baz"].bar`, 670 &Reference{ 671 Subject: ResourceInstance{ 672 Resource: Resource{ 673 Mode: ManagedResourceMode, 674 Type: "boop_instance", 675 Name: "foo", 676 }, 677 Key: StringKey("baz"), 678 }, 679 SourceRange: hcl.Range{ 680 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 681 End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 682 }, 683 Remaining: hcl.Traversal{ 684 hcl.TraverseAttr{ 685 Name: "bar", 686 SrcRange: hcl.Range{ 687 Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 688 End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, 689 }, 690 }, 691 }, 692 }, 693 ``, 694 }, 695 { 696 `boop_instance.foo["baz"]`, 697 &Reference{ 698 Subject: ResourceInstance{ 699 Resource: Resource{ 700 Mode: ManagedResourceMode, 701 Type: "boop_instance", 702 Name: "foo", 703 }, 704 Key: StringKey("baz"), 705 }, 706 SourceRange: hcl.Range{ 707 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 708 End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 709 }, 710 }, 711 ``, 712 }, 713 { 714 `boop_instance`, 715 nil, 716 `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, 717 }, 718 } 719 720 for _, test := range tests { 721 t.Run(test.Input, func(t *testing.T) { 722 traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1}) 723 if travDiags.HasErrors() { 724 t.Fatal(travDiags.Error()) 725 } 726 727 got, diags := ParseRef(traversal) 728 729 switch len(diags) { 730 case 0: 731 if test.WantErr != "" { 732 t.Fatalf("succeeded; want error: %s", test.WantErr) 733 } 734 case 1: 735 if test.WantErr == "" { 736 t.Fatalf("unexpected diagnostics: %s", diags) 737 } 738 if got, want := diags[0].Detail, test.WantErr; got != want { 739 t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) 740 } 741 default: 742 t.Fatalf("too many diagnostics: %s", diags) 743 } 744 745 if diags.HasErrors() { 746 return 747 } 748 749 for _, problem := range deep.Equal(got, test.Want) { 750 t.Errorf(problem) 751 } 752 }) 753 } 754 }