Commit 9a7b1c1

mo khan <mo@mokhan.ca>
2021-05-20 19:13:36
feat: parse multiple provider blocks
1 parent 2b5437f
Changed files (4)
lib
spandx
terraform
parsers
spec
fixtures
terraform
unit
terraform
lib/spandx/terraform/parsers/hcl.rb
@@ -6,6 +6,7 @@ module Spandx
       class Hcl < Parslet::Parser
         rule(:alpha) { match['a-zA-Z'] }
         rule(:assign) { str('=') }
+        rule(:colon) { str(':') }
         rule(:comma) { str(',') }
         rule(:comment) { (str('#') | str('//')) >> ((str("\n") >> str("\r").maybe).absent? >> any).repeat >> eol }
         rule(:crlf) { match('[\r\n]') }
@@ -21,11 +22,13 @@ module Spandx
         rule(:major_minor_patch) { number >> dot >> number >> dot >> number }
         rule(:multiline_comment) { str('/*') >> (str('*/').absent? >> any).repeat >> str('*/') }
         rule(:number) { digit.repeat }
+        rule(:plus) { str('+') }
         rule(:pre_release) { hyphen >> (alpha | digit).repeat }
         rule(:pre_release?) { pre_release.maybe }
         rule(:quote) { str('"') }
         rule(:rbracket) { str(']') }
         rule(:rcurly) { str('}') }
+        rule(:slash) { str('/') }
         rule(:space) { match('\s') }
         rule(:tilda_wacka) { str('~>') }
         rule(:version) { number >> dot >> number >> dot >> number >> pre_release? }
@@ -61,39 +64,41 @@ module Spandx
         end
 
         rule :string do
-          quote >> match('[0-9A-Za-z.~> :=/]').repeat.as(:value) >> quote
+          quote >> (
+            digit | dot | alpha | str('~> ') | slash | colon | assign | plus
+          ).repeat(1).as(:value) >> quote
         end
 
         rule :array_item do
-          whitespace >> string >> comma >> eol
+          whitespace? >> string >> comma.maybe >> eol
         end
 
         rule :array do
-          lbracket >> eol >> array_item.repeat >> rbracket
+          lbracket >> eol >> array_item.repeat >> whitespace >> rbracket
         end
 
-        rule :argument do
-          alpha.repeat.as(:name) >> whitespace >> assign >> whitespace >> (array.as(:values) | string)
+        rule :argument_value do
+          (array.as(:values) | string) >> eol
         end
 
-        rule :arguments do
-          (argument >> eol).repeat
+        rule :argument do
+          whitespace >> alpha.repeat(1).as(:name) >> whitespace >> assign >> whitespace >> argument_value
         end
 
-        rule :identifier do
-          whitespace >> quote >> ((alpha | match('[./]')).repeat).as(:name) >> quote >> whitespace
+        rule :block_body do
+          lcurly >> crlf >> argument.repeat.as(:arguments) >> rcurly
         end
 
-        rule :block_body do
-          arguments.as(:arguments)
+        rule :identifier do
+          whitespace >> quote >> (alpha | dot | slash).repeat(1).as(:name) >> quote >> whitespace
         end
 
         rule :block do
-          whitespace? >> (alpha.repeat).as(:type) >> identifier >> whitespace >> lcurly >> eol >> block_body >> rcurly >> eol
+          alpha.repeat(1).as(:type) >> identifier >> block_body
         end
 
         rule :blocks do
-          block.repeat.as(:blocks)
+          whitespace? >> (block >> eol.maybe).repeat(1).as(:blocks)
         end
 
         root(:blocks)
spec/fixtures/terraform/multiple_providers/.terraform.lock.hcl
@@ -0,0 +1,40 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/aws" {
+  version     = "3.40.0"
+  constraints = "~> 3.27"
+  hashes = [
+    "h1:0r9TS3qACD9xJhrfTPZR7ygoCKDWHRX4c0D5GCyfAu4=",
+    "zh:2fd824991b19837e200d19b17d8188bf71efb92c36511809484549e77b4045dd",
+    "zh:47250cb58b3bd6f2698ca17bfb962710542d6adf95637cd560f6119abf97dba2",
+    "zh:515722a8c8726541b05362ec71331264977603374a2e4d4d64f89940873143ea",
+    "zh:61b6b7542da2113278c987a0af9f230321f5ed605f1e3098824603cb09ac771b",
+    "zh:66aad13ada6344b64adbc67abad4f35c414e62838a99f78626befb8b74c760d8",
+    "zh:7d4436aeb53fa348d7fd3c2ab4a727b03c7c59bfdcdecef4a75237760f3bb3cf",
+    "zh:a4583891debc49678491510574b1c28bb4fe3f83ed2bb353959c4c1f6f409f1f",
+    "zh:b8badecea52f6996ae832144560be87e0b7c2da7fe1dcd6e6230969234b2fc55",
+    "zh:cecf64a085f640c30437ccc31bd964c21004ae8ae00cfbd95fb04037e46b88ca",
+    "zh:d81dbb9ad8ce5eca4d1fc5a7a06bbb9c47ea8691f1502e94760fa680e20e4afc",
+    "zh:f0fc724a964c7f8154bc5911d572ee411f5d181414f9b1f09de7ebdacb0d884b",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/azurerm" {
+  version     = "2.59.0"
+  constraints = "~> 2.1"
+  hashes = [
+    "h1:Mp7ECMHocobalN1+ASSKG5dHB7RnhZ6Y0rEEFTT5urA=",
+    "zh:0996d1c85bccdb15aeb6bc32f763c2d85ff854b33c3c3d62c34859669e05785e",
+    "zh:37807677e68058381514897ce10dc73a0dd0f503aba98113ac79844d310010e3",
+    "zh:3bccf9215bdbcc89327582b1d9d2a633c59215ca6452dbb4f9d0a7a661074c5b",
+    "zh:4801791332ab81e51d1ead47a62e0081ec4c1f23ef0fc2e8b15fef315ecdf07a",
+    "zh:5bad44816a3eaeb335f665f6eef9b41a403a40e9bddb2db8406ab0e847f639ca",
+    "zh:64f79c4ddc2bf8384f1a42c4e430ffdc53cb1fbc565bfe1cdc6b075dcdf098e9",
+    "zh:75c96fcb592ed80cc403944faadda25aeadda7fd6de9162a8d365249b1ec1c17",
+    "zh:8604558f2f201eefe25f4c611a5d4ef4d7c75338bf2f4a6321da5caa94937947",
+    "zh:cab930e374d33b3b980c6774f3d0ac3e3d7e1e596aba586d4368d8bcf05cf9c5",
+    "zh:cf0e78eb1e84b6dd11031283878e392e55801e3acd9c5592309e6f76ebe3a621",
+    "zh:eba02fcab150775b8b8beeec0c7dbba1585a57f4e97272f48c71021c5e289579",
+  ]
+}
spec/fixtures/terraform/multiple_providers/main.tf
@@ -0,0 +1,12 @@
+terraform {
+  required_providers {
+    aws = {
+      source = "hashicorp/aws"
+      version = "~> 3.27"
+    }
+    azure = {
+      source = "hashicorp/azurerm"
+      version = "~> 2.1"
+    }
+  }
+}
spec/unit/terraform/parsers/hcl_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Spandx::Terraform::Parsers::Hcl do
   describe '#parse' do
     subject { parser.parse_with_debug(content) }
 
-    context 'when parsing an empty provider block' do
+    context 'when parsing a single provider' do
       let(:content) do
         <<~HCL
           # This file is maintained automatically by "terraform init".
@@ -66,6 +66,64 @@ RSpec.describe Spandx::Terraform::Parsers::Hcl do
         ])
       end
     end
+
+    context 'when parsing multiple provider blocks' do
+      let(:content) { fixture_file_content('terraform/multiple_providers/.terraform.lock.hcl') }
+
+      specify { expect(subject).to be_truthy }
+      specify { expect(subject[:blocks][0][:name].to_s).to eql('registry.terraform.io/hashicorp/aws') }
+      specify { expect(subject[:blocks][0][:type].to_s).to eql('provider') }
+      specify { expect(subject[:blocks][1][:name].to_s).to eql('registry.terraform.io/hashicorp/azurerm') }
+      specify { expect(subject[:blocks][1][:type].to_s).to eql('provider') }
+
+      specify do
+        expect(subject[:blocks][0][:arguments]).to match_array([
+          { name: 'version', value: '3.40.0' },
+          { name: 'constraints', value: '~> 3.27' },
+          {
+            name: 'hashes',
+            values: [
+              { value: 'h1:0r9TS3qACD9xJhrfTPZR7ygoCKDWHRX4c0D5GCyfAu4=' },
+              { value: 'zh:2fd824991b19837e200d19b17d8188bf71efb92c36511809484549e77b4045dd' },
+              { value: 'zh:47250cb58b3bd6f2698ca17bfb962710542d6adf95637cd560f6119abf97dba2' },
+              { value: 'zh:515722a8c8726541b05362ec71331264977603374a2e4d4d64f89940873143ea' },
+              { value: 'zh:61b6b7542da2113278c987a0af9f230321f5ed605f1e3098824603cb09ac771b' },
+              { value: 'zh:66aad13ada6344b64adbc67abad4f35c414e62838a99f78626befb8b74c760d8' },
+              { value: 'zh:7d4436aeb53fa348d7fd3c2ab4a727b03c7c59bfdcdecef4a75237760f3bb3cf' },
+              { value: 'zh:a4583891debc49678491510574b1c28bb4fe3f83ed2bb353959c4c1f6f409f1f' },
+              { value: 'zh:b8badecea52f6996ae832144560be87e0b7c2da7fe1dcd6e6230969234b2fc55' },
+              { value: 'zh:cecf64a085f640c30437ccc31bd964c21004ae8ae00cfbd95fb04037e46b88ca' },
+              { value: 'zh:d81dbb9ad8ce5eca4d1fc5a7a06bbb9c47ea8691f1502e94760fa680e20e4afc' },
+              { value: 'zh:f0fc724a964c7f8154bc5911d572ee411f5d181414f9b1f09de7ebdacb0d884b' },
+            ]
+          },
+        ])
+      end
+
+      specify do
+        expect(subject[:blocks][1][:arguments]).to match_array([
+          { name: 'version', value: '2.59.0' },
+          { name: 'constraints', value: '~> 2.1' },
+          {
+            name: 'hashes',
+            values: [
+              { value: 'h1:Mp7ECMHocobalN1+ASSKG5dHB7RnhZ6Y0rEEFTT5urA=' },
+              { value: 'zh:0996d1c85bccdb15aeb6bc32f763c2d85ff854b33c3c3d62c34859669e05785e' },
+              { value: 'zh:37807677e68058381514897ce10dc73a0dd0f503aba98113ac79844d310010e3' },
+              { value: 'zh:3bccf9215bdbcc89327582b1d9d2a633c59215ca6452dbb4f9d0a7a661074c5b' },
+              { value: 'zh:4801791332ab81e51d1ead47a62e0081ec4c1f23ef0fc2e8b15fef315ecdf07a' },
+              { value: 'zh:5bad44816a3eaeb335f665f6eef9b41a403a40e9bddb2db8406ab0e847f639ca' },
+              { value: 'zh:64f79c4ddc2bf8384f1a42c4e430ffdc53cb1fbc565bfe1cdc6b075dcdf098e9' },
+              { value: 'zh:75c96fcb592ed80cc403944faadda25aeadda7fd6de9162a8d365249b1ec1c17' },
+              { value: 'zh:8604558f2f201eefe25f4c611a5d4ef4d7c75338bf2f4a6321da5caa94937947' },
+              { value: 'zh:cab930e374d33b3b980c6774f3d0ac3e3d7e1e596aba586d4368d8bcf05cf9c5' },
+              { value: 'zh:cf0e78eb1e84b6dd11031283878e392e55801e3acd9c5592309e6f76ebe3a621' },
+              { value: 'zh:eba02fcab150775b8b8beeec0c7dbba1585a57f4e97272f48c71021c5e289579' },
+            ]
+          },
+        ])
+      end
+    end
   end
 
   describe '#version_assignment' do
@@ -133,6 +191,7 @@ RSpec.describe Spandx::Terraform::Parsers::Hcl do
   end
 
   (0..9).each { |digit| specify { expect(parser.digit).to parse(digit.to_s) } }
+  specify { expect(parser.assign).not_to parse('==') }
   specify { expect(parser.assign).to parse('=') }
   specify { expect(parser.comment).to parse('# Manual edits may be lost in future updates.') }
   specify { expect(parser.comment).to parse('# This file is maintained automatically by "terraform init".') }
@@ -149,6 +208,8 @@ RSpec.describe Spandx::Terraform::Parsers::Hcl do
   specify { expect(parser.space).to parse(' ') }
   specify { expect(parser.whitespace).to parse('# This is a comment') }
   specify { expect(parser.whitespace).to parse('// This is a comment') }
+  specify { expect(parser.string).to parse('"h1:fjlp3Pd3QsTLghNm7TUh/KnEMM2D3tLb7jsDLs8oWUE="') }
+  specify { expect(parser.string).to parse('"zh:2014b397dd93fa55f2f2d1338c19e5b2b77b025a76a6b1fceea0b8696e984b9c"') }
 
   specify do
     expect(parser.whitespace).to parse(<<~HCL)
@@ -179,4 +240,141 @@ RSpec.describe Spandx::Terraform::Parsers::Hcl do
       ]
     HCL
   end
+
+  specify do
+    expect(parser.block).to parse(<<~HCL.chomp)
+      provider "thing" {
+        argument = "value"
+        arguments = [
+          "value",
+          "value",
+        ]
+      }
+    HCL
+  end
+
+  specify do
+    expect(parser.block_body.parse_with_debug(<<~HCL.chomp)).not_to be_nil
+      {
+        argument = "value"
+        arguments = [
+          "value",
+          "value",
+        ]
+      }
+    HCL
+  end
+
+  specify { expect(parser.argument).to parse('argument = "value"') }
+
+  specify do
+    expect(parser.argument).to parse(<<~HCL)
+      arguments = [
+        "a",
+        "b",
+      ]
+    HCL
+  end
+
+  describe '#blocks' do
+    subject { parser.blocks.parse_with_debug(hcl) }
+
+    context 'when parsing multiple multi-line empty blocks' do
+      let(:hcl) do
+        <<~HCL
+          provider "thingy" {
+          }
+
+          provider "other.thingy" {
+          }
+        HCL
+      end
+
+      it 'parses multiple empty blocks' do
+        expect(subject[:blocks]).to match_array([
+          { type: 'provider', name: 'thingy', arguments: [] },
+          { type: 'provider', name: 'other.thingy', arguments: [] },
+        ])
+      end
+    end
+
+    context 'when parsing multiple multi-line blocks with one argument assignment to a string in the first block' do
+      let(:hcl) do
+        <<~HCL
+          provider "thingy" {
+            name = "blah"
+          }
+
+          provider "other.thingy" {
+          }
+        HCL
+      end
+
+      it 'parses multiple empty blocks' do
+        expect(subject[:blocks]).to match_array([
+          { type: 'provider', name: 'thingy', arguments: [{ name: 'name', value: 'blah' }] },
+          { type: 'provider', name: 'other.thingy', arguments: [] },
+        ])
+      end
+    end
+
+    context 'when parsing multiple multi-line blocks with one argument assignment to a string in the second block' do
+      let(:hcl) do
+        <<~HCL
+          provider "thingy" {
+          }
+
+          provider "other.thingy" {
+            name = "blah"
+          }
+        HCL
+      end
+
+      it 'parses multiple empty blocks' do
+        expect(subject[:blocks]).to match_array([
+          { type: 'provider', name: 'thingy', arguments: [] },
+          { type: 'provider', name: 'other.thingy', arguments: [{ name: 'name', value: 'blah' }] },
+        ])
+      end
+    end
+
+    context 'when parsing a blocks with one assignment to an empty array' do
+      let(:hcl) do
+        <<~HCL
+          provider "thingy" {
+            names = [
+            ]
+          }
+        HCL
+      end
+
+      pending 'parses multiple empty blocks' do
+        expect(subject[:blocks]).to match_array([
+          { type: 'provider', name: 'thingy', arguments: [{ name: 'names', values: [] }] },
+        ])
+      end
+    end
+
+    context 'when parsing multiple multi-line blocks with one assignment to a multi-line array' do
+      let(:hcl) do
+        <<~HCL
+          provider "thingy" {
+            names = [
+              "blah"
+            ]
+          }
+
+          provider "other.thingy" {
+          }
+        HCL
+      end
+
+      it 'parses multiple empty blocks' do
+        expect(subject[:blocks]).to match_array([
+          { type: 'provider', name: 'thingy', arguments: [{ name: 'names', values: [{ value: 'blah' }] }] },
+          { type: 'provider', name: 'other.thingy', arguments: [] },
+        ])
+      end
+    end
+  end
 end