main
  1# frozen_string_literal: true
  2
  3RSpec.describe Hcl2::Parser do
  4  subject(:parser) { described_class.new }
  5
  6  describe "#parse" do
  7    subject { parser.parse_with_debug(content) }
  8
  9    context "when parsing a single provider" do
 10      let(:content) do
 11        <<~HCL
 12          # This file is maintained automatically by "terraform init".
 13          # Manual edits may be lost in future updates.
 14
 15          provider "registry.terraform.io/hashicorp/aws" {
 16            version     = "3.39.0"
 17            constraints = "~> 3.27"
 18            hashes = [
 19              "h1:fjlp3Pd3QsTLghNm7TUh/KnEMM2D3tLb7jsDLs8oWUE=",
 20              "zh:2014b397dd93fa55f2f2d1338c19e5b2b77b025a76a6b1fceea0b8696e984b9c",
 21              "zh:23d59c68ab50148a0f5c911a801734e9934a1fccd41118a8efb5194135cbd360",
 22              "zh:412eab41d4934ca9c47083faa128e4cd585c3bb44ad718e40d67091aebc02f4e",
 23              "zh:4b75e0a259b56d97e66b7d69f3f25bd4cc7be2440c0fe35529f46de7d40a49d3",
 24              "zh:694a32519dcca5bd8605d06481d16883d55160d97c1f4039deb13c6ca8de8427",
 25              "zh:6a0bcef43c2d9a97aeaaac3c5d1d6728dc2464a51a014f118c691c79029d0903",
 26              "zh:6d78fc7c663247ca2a80f276008dcdafece4cac75e2639bbce188c08b796040a",
 27              "zh:78f846a505d7b64b67feed1527d4d2b40130dadaf8e3112113685e148f49b156",
 28              "zh:881bc969432d3ef6ec70f5a762c3415e037904338579b0a360c6818b74d26e59",
 29              "zh:96c1ca80c1d693a3eef80489adb45c076ee8e6878e461d6c29b05388d4b95f48",
 30              "zh:9be5fa342272586fc6e319e20f21c0c5c801b05dcf7d59e473ad0882c9ecfa70",
 31            ]
 32          }
 33
 34          /*
 35            This is a multi-line comment
 36            that spans multiple lines
 37          */
 38        HCL
 39      end
 40
 41      specify { expect(subject).to be_truthy }
 42      specify { expect(subject[:blocks][0][:name].to_s).to eql("registry.terraform.io/hashicorp/aws") }
 43      specify { expect(subject[:blocks][0][:type].to_s).to eql("provider") }
 44
 45      specify do
 46        expect(subject[:blocks][0][:arguments]).to match_array([
 47          { name: "version", value: "3.39.0" },
 48          { name: "constraints", value: "~> 3.27" },
 49          {
 50            name: "hashes",
 51            values: [
 52              { value: "h1:fjlp3Pd3QsTLghNm7TUh/KnEMM2D3tLb7jsDLs8oWUE=" },
 53              { value: "zh:2014b397dd93fa55f2f2d1338c19e5b2b77b025a76a6b1fceea0b8696e984b9c" },
 54              { value: "zh:23d59c68ab50148a0f5c911a801734e9934a1fccd41118a8efb5194135cbd360" },
 55              { value: "zh:412eab41d4934ca9c47083faa128e4cd585c3bb44ad718e40d67091aebc02f4e" },
 56              { value: "zh:4b75e0a259b56d97e66b7d69f3f25bd4cc7be2440c0fe35529f46de7d40a49d3" },
 57              { value: "zh:694a32519dcca5bd8605d06481d16883d55160d97c1f4039deb13c6ca8de8427" },
 58              { value: "zh:6a0bcef43c2d9a97aeaaac3c5d1d6728dc2464a51a014f118c691c79029d0903" },
 59              { value: "zh:6d78fc7c663247ca2a80f276008dcdafece4cac75e2639bbce188c08b796040a" },
 60              { value: "zh:78f846a505d7b64b67feed1527d4d2b40130dadaf8e3112113685e148f49b156" },
 61              { value: "zh:881bc969432d3ef6ec70f5a762c3415e037904338579b0a360c6818b74d26e59" },
 62              { value: "zh:96c1ca80c1d693a3eef80489adb45c076ee8e6878e461d6c29b05388d4b95f48" },
 63              { value: "zh:9be5fa342272586fc6e319e20f21c0c5c801b05dcf7d59e473ad0882c9ecfa70" },
 64            ]
 65          },
 66        ])
 67      end
 68    end
 69
 70    context "when parsing multiple provider blocks" do
 71      let(:content) { fixture_file_content("terraform/multiple_providers/.terraform.lock.hcl") }
 72
 73      specify { expect(subject).to be_truthy }
 74      specify { expect(subject[:blocks][0][:name].to_s).to eql("registry.terraform.io/hashicorp/aws") }
 75      specify { expect(subject[:blocks][0][:type].to_s).to eql("provider") }
 76      specify { expect(subject[:blocks][1][:name].to_s).to eql("registry.terraform.io/hashicorp/azurerm") }
 77      specify { expect(subject[:blocks][1][:type].to_s).to eql("provider") }
 78
 79      specify do
 80        expect(subject[:blocks][0][:arguments]).to match_array([
 81          { name: "version", value: "3.40.0" },
 82          { name: "constraints", value: "~> 3.27" },
 83          {
 84            name: "hashes",
 85            values: [
 86              { value: "h1:0r9TS3qACD9xJhrfTPZR7ygoCKDWHRX4c0D5GCyfAu4=" },
 87              { value: "zh:2fd824991b19837e200d19b17d8188bf71efb92c36511809484549e77b4045dd" },
 88              { value: "zh:47250cb58b3bd6f2698ca17bfb962710542d6adf95637cd560f6119abf97dba2" },
 89              { value: "zh:515722a8c8726541b05362ec71331264977603374a2e4d4d64f89940873143ea" },
 90              { value: "zh:61b6b7542da2113278c987a0af9f230321f5ed605f1e3098824603cb09ac771b" },
 91              { value: "zh:66aad13ada6344b64adbc67abad4f35c414e62838a99f78626befb8b74c760d8" },
 92              { value: "zh:7d4436aeb53fa348d7fd3c2ab4a727b03c7c59bfdcdecef4a75237760f3bb3cf" },
 93              { value: "zh:a4583891debc49678491510574b1c28bb4fe3f83ed2bb353959c4c1f6f409f1f" },
 94              { value: "zh:b8badecea52f6996ae832144560be87e0b7c2da7fe1dcd6e6230969234b2fc55" },
 95              { value: "zh:cecf64a085f640c30437ccc31bd964c21004ae8ae00cfbd95fb04037e46b88ca" },
 96              { value: "zh:d81dbb9ad8ce5eca4d1fc5a7a06bbb9c47ea8691f1502e94760fa680e20e4afc" },
 97              { value: "zh:f0fc724a964c7f8154bc5911d572ee411f5d181414f9b1f09de7ebdacb0d884b" },
 98            ]
 99          },
100        ])
101      end
102
103      specify do
104        expect(subject[:blocks][1][:arguments]).to match_array([
105          { name: "version", value: "2.59.0" },
106          { name: "constraints", value: "~> 2.1" },
107          {
108            name: "hashes",
109            values: [
110              { value: "h1:Mp7ECMHocobalN1+ASSKG5dHB7RnhZ6Y0rEEFTT5urA=" },
111              { value: "zh:0996d1c85bccdb15aeb6bc32f763c2d85ff854b33c3c3d62c34859669e05785e" },
112              { value: "zh:37807677e68058381514897ce10dc73a0dd0f503aba98113ac79844d310010e3" },
113              { value: "zh:3bccf9215bdbcc89327582b1d9d2a633c59215ca6452dbb4f9d0a7a661074c5b" },
114              { value: "zh:4801791332ab81e51d1ead47a62e0081ec4c1f23ef0fc2e8b15fef315ecdf07a" },
115              { value: "zh:5bad44816a3eaeb335f665f6eef9b41a403a40e9bddb2db8406ab0e847f639ca" },
116              { value: "zh:64f79c4ddc2bf8384f1a42c4e430ffdc53cb1fbc565bfe1cdc6b075dcdf098e9" },
117              { value: "zh:75c96fcb592ed80cc403944faadda25aeadda7fd6de9162a8d365249b1ec1c17" },
118              { value: "zh:8604558f2f201eefe25f4c611a5d4ef4d7c75338bf2f4a6321da5caa94937947" },
119              { value: "zh:cab930e374d33b3b980c6774f3d0ac3e3d7e1e596aba586d4368d8bcf05cf9c5" },
120              { value: "zh:cf0e78eb1e84b6dd11031283878e392e55801e3acd9c5592309e6f76ebe3a621" },
121              { value: "zh:eba02fcab150775b8b8beeec0c7dbba1585a57f4e97272f48c71021c5e289579" },
122            ]
123          },
124        ])
125      end
126    end
127
128    context "when parsing a module with a git source" do
129      let(:content) do
130        <<~HCL
131          module "origin_label" {
132            source     = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.3.7"
133          }
134        HCL
135      end
136
137      specify { expect(subject).to be_truthy }
138
139      specify do
140        expect(subject[:blocks][0][:type].to_s).to eq("module")
141        expect(subject[:blocks][0][:name].to_s).to eq("origin_label")
142        expect(subject[:blocks][0][:arguments][0][:name].to_s).to eq("source")
143        expect(subject[:blocks][0][:arguments][0][:value].to_s).to eq("git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.3.7")
144      end
145    end
146
147    context "when parsing a module with an argument assignment via string interpolation" do
148      let(:content) do
149        <<~HCL
150          module "origin_label" {
151            namespace  = "${var.namespace}"
152          }
153        HCL
154      end
155
156      specify { expect(subject).to be_truthy }
157    end
158
159    context "when parsing a module with an argument assigned to a single line array declaration" do
160      let(:content) do
161        <<~HCL
162          module "origin_label" {
163            attributes = ["${compact(concat(var.attributes, list("origin")))}"]
164          }
165        HCL
166      end
167
168      pending { expect(subject).to be_truthy }
169    end
170
171    context "when parsing a resource with multiple names" do
172      let(:content) do
173        <<~HCL
174          resource "aws_cloudfront_origin_access_identity" "default" {
175            comment = "${module.distribution_label.id}"
176          }
177        HCL
178      end
179
180      pending { expect(subject).to be_truthy }
181    end
182
183    context "when parsing a resource with a nested block" do
184      let(:content) do
185        <<~HCL
186          resource "aws_cloudfront_distribution" "default" {
187            enabled             = "${var.enabled}"
188            is_ipv6_enabled     = "${var.is_ipv6_enabled}"
189            comment             = "${var.comment}"
190            default_root_object = "${var.default_root_object}"
191            price_class         = "${var.price_class}"
192
193            logging_config = {
194              include_cookies = "${var.log_include_cookies}"
195              bucket          = "${module.logs.bucket_domain_name}"
196              prefix          = "${var.log_prefix}"
197            }
198          }
199        HCL
200      end
201
202      pending { expect(subject).to be_truthy }
203    end
204
205    context "when parsing a resource with an assignment to another variable" do
206      let(:content) do
207        <<~HCL
208          module "distribution_label" {
209            source     = "bitbucket.org/cloudposse/terraform-null-label.git"
210            namespace  = var.namespace
211          }
212        HCL
213      end
214
215      pending { expect(subject).to be_truthy }
216    end
217  end
218
219  describe "#version_assignment" do
220    subject { parser.version_assignment }
221
222    [
223      'version     = "3.39.0"',
224      'version     = "3.39.0-alpha"',
225      'version     = "3.39.0-beta"',
226      'version     = "3.39.0-d15aad9f"',
227      'version     = "3.39.0-d15aad9f6ad69c4248a70b11a6534c1c841ec6f9"',
228      'version = "3.39.0"',
229      'version = "3.39.0-alpha"',
230      'version = "3.39.0-beta"',
231      'version = "3.39.0-d15aad9f"',
232      'version = "3.39.0-d15aad9f6ad69c4248a70b11a6534c1c841ec6f9"',
233    ].each do |raw|
234      specify { expect(subject).to parse(raw) }
235    end
236  end
237
238  describe "#version" do
239    [
240      "0.1.1-alpha",
241      "0.1.1-beta",
242      "1.2.3",
243      "3.39.0",
244      "3.39.0-d15aad9f",
245      "3.39.0-d15aad9f6ad69c4248a70b11a6534c1c841ec6f9",
246    ].each do |raw|
247      specify { expect(parser.version).to parse(raw) }
248    end
249  end
250
251  describe "#constraint_assignment" do
252    subject { parser.constraint_assignment }
253
254    [
255      'constraints = ">= 3"',
256      'constraints = ">= 3.27"',
257      'constraints = ">= 3.27.0"',
258      'constraints = "~> 3"',
259      'constraints = "~> 3.27"',
260      'constraints = "~> 3.27.0"',
261    ].each do |raw|
262      specify { expect(subject).to parse(raw) }
263    end
264  end
265
266  describe "#version_constraint" do
267    [
268      "~> 3",
269      "~> 3.27",
270      "~> 3.27.0",
271      ">= 1.2.0",
272      ">= 1.20",
273      ">= 10",
274    ].each do |raw|
275      specify { expect(parser.version_constraint).to parse(raw) }
276    end
277  end
278
279  (("a".."z").to_a + ("A".."Z").to_a).each do |letter|
280    specify { expect(parser.alpha).to parse(letter) }
281  end
282
283  (0..9).each { |digit| specify { expect(parser.digit).to parse(digit.to_s) } }
284  specify { expect(parser.assign).not_to parse("==") }
285  specify { expect(parser.assign).to parse("=") }
286  specify { expect(parser.comment).to parse("# Manual edits may be lost in future updates.") }
287  specify { expect(parser.comment).to parse('# This file is maintained automatically by "terraform init".') }
288  specify { expect(parser.comment).to parse('// This file is maintained automatically by "terraform init".') }
289  specify { expect(parser.crlf).to parse("\n") }
290  specify { expect(parser.crlf).to parse("\r") }
291  specify { expect(parser.dot).to parse(".") }
292  specify { expect(parser.eol).to parse(" \n") }
293  specify { expect(parser.eol).to parse("\n") }
294  specify { expect(parser.lcurly).to parse("{") }
295  specify { expect(parser.number).to parse("123") }
296  specify { expect(parser.quote).to parse('"') }
297  specify { expect(parser.rcurly).to parse("}") }
298  specify { expect(parser.space).to parse(" ") }
299  specify { expect(parser.whitespace).to parse("# This is a comment") }
300  specify { expect(parser.whitespace).to parse("// This is a comment") }
301  specify { expect(parser.string).to parse('"h1:fjlp3Pd3QsTLghNm7TUh/KnEMM2D3tLb7jsDLs8oWUE="') }
302  specify { expect(parser.string).to parse('"zh:2014b397dd93fa55f2f2d1338c19e5b2b77b025a76a6b1fceea0b8696e984b9c"') }
303
304  specify do
305    expect(parser.empty_array).to parse(<<~HCL.chomp)
306      [
307      ]
308    HCL
309  end
310
311  specify do
312    expect(parser.whitespace).to parse(<<~HCL)
313      /*
314        This is a multi-line comment
315        that spans multiple lines
316      */
317    HCL
318  end
319
320  specify { expect(parser.argument).to parse('constraints = "~> 3.27"') }
321
322  specify do
323    expect(parser.argument).to parse(<<~HCL.chomp)
324      hashes = [
325        "h1:fjlp3Pd3QsTLghNm7TUh/KnEMM2D3tLb7jsDLs8oWUE=",
326        "zh:2014b397dd93fa55f2f2d1338c19e5b2b77b025a76a6b1fceea0b8696e984b9c",
327        "zh:23d59c68ab50148a0f5c911a801734e9934a1fccd41118a8efb5194135cbd360",
328        "zh:412eab41d4934ca9c47083faa128e4cd585c3bb44ad718e40d67091aebc02f4e",
329        "zh:4b75e0a259b56d97e66b7d69f3f25bd4cc7be2440c0fe35529f46de7d40a49d3",
330        "zh:694a32519dcca5bd8605d06481d16883d55160d97c1f4039deb13c6ca8de8427",
331        "zh:6a0bcef43c2d9a97aeaaac3c5d1d6728dc2464a51a014f118c691c79029d0903",
332        "zh:6d78fc7c663247ca2a80f276008dcdafece4cac75e2639bbce188c08b796040a",
333        "zh:78f846a505d7b64b67feed1527d4d2b40130dadaf8e3112113685e148f49b156",
334        "zh:881bc969432d3ef6ec70f5a762c3415e037904338579b0a360c6818b74d26e59",
335        "zh:96c1ca80c1d693a3eef80489adb45c076ee8e6878e461d6c29b05388d4b95f48",
336        "zh:9be5fa342272586fc6e319e20f21c0c5c801b05dcf7d59e473ad0882c9ecfa70",
337      ]
338    HCL
339  end
340
341  specify do
342    expect(parser.block).to parse(<<~HCL.chomp)
343      provider "thing" {
344        argument = "value"
345        arguments = [
346          "value",
347          "value",
348        ]
349      }
350    HCL
351  end
352
353  specify do
354    expect(parser.block_body.parse_with_debug(<<~HCL.chomp)).not_to be_nil
355      {
356        argument = "value"
357        arguments = [
358          "value",
359          "value",
360        ]
361      }
362    HCL
363  end
364
365  specify { expect(parser.argument).to parse('argument = "value"') }
366
367  specify do
368    expect(parser.argument).to parse(<<~HCL)
369      arguments = [
370        "a",
371        "b",
372      ]
373    HCL
374  end
375
376  describe "#blocks" do
377    subject { parser.blocks.parse_with_debug(hcl) }
378
379    context "when parsing multiple multi-line empty blocks" do
380      let(:hcl) do
381        <<~HCL
382          provider "thingy" {
383          }
384
385          provider "other.thingy" {
386          }
387        HCL
388      end
389
390      it "parses multiple empty blocks" do
391        expect(subject[:blocks]).to match_array([
392          { type: "provider", name: "thingy", arguments: [] },
393          { type: "provider", name: "other.thingy", arguments: [] },
394        ])
395      end
396    end
397
398    context "when parsing multiple multi-line blocks with one argument assignment to a string in the first block" do
399      let(:hcl) do
400        <<~HCL
401          provider "thingy" {
402            name = "blah"
403          }
404
405          provider "other.thingy" {
406          }
407        HCL
408      end
409
410      it "parses multiple empty blocks" do
411        expect(subject[:blocks]).to match_array([
412          { type: "provider", name: "thingy", arguments: [{ name: "name", value: "blah" }] },
413          { type: "provider", name: "other.thingy", arguments: [] },
414        ])
415      end
416    end
417
418    context "when parsing multiple multi-line blocks with one argument assignment to a string in the second block" do
419      let(:hcl) do
420        <<~HCL
421          provider "thingy" {
422          }
423
424          provider "other.thingy" {
425            name = "blah"
426          }
427        HCL
428      end
429
430      it "parses multiple empty blocks" do
431        expect(subject[:blocks]).to match_array([
432          { type: "provider", name: "thingy", arguments: [] },
433          { type: "provider", name: "other.thingy", arguments: [{ name: "name", value: "blah" }] },
434        ])
435      end
436    end
437
438    context "when parsing a blocks with one assignment to an empty array" do
439      let(:hcl) do
440        <<~HCL
441          provider "thingy" {
442            names = [
443            ]
444          }
445        HCL
446      end
447
448      it "parses multiple empty blocks" do
449        expect(subject[:blocks]).to match_array([
450          { type: "provider", name: "thingy", arguments: [{ name: "names" }] },
451        ])
452      end
453    end
454
455    context "when parsing multiple multi-line blocks with one assignment to a multi-line array" do
456      let(:hcl) do
457        <<~HCL
458          provider "thingy" {
459            names = [
460              "blah"
461            ]
462          }
463
464          provider "other.thingy" {
465          }
466        HCL
467      end
468
469      it "parses multiple empty blocks" do
470        expect(subject[:blocks]).to match_array([
471          { type: "provider", name: "thingy", arguments: [{ name: "names", values: [{ value: "blah" }] }] },
472          { type: "provider", name: "other.thingy", arguments: [] },
473        ])
474      end
475    end
476  end
477end