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