puppet-lint 2.3.6 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ require 'strscan'
2
+
3
+ class PuppetLint
4
+ class Lexer
5
+ # Document this
6
+ # TODO
7
+ class StringSlurper
8
+ class UnterminatedStringError < StandardError; end
9
+
10
+ attr_accessor :scanner
11
+ attr_accessor :results
12
+ attr_accessor :interp_stack
13
+
14
+ START_INTERP_PATTERN = %r{\$\{}
15
+ END_INTERP_PATTERN = %r{\}}
16
+ END_STRING_PATTERN = %r{(\A|[^\\])(\\\\)*"}
17
+ UNENC_VAR_PATTERN = %r{(\A|[^\\])\$(::)?(\w+(-\w+)*::)*\w+(-\w+)*}
18
+ ESC_DQUOTE_PATTERN = %r{\\+"}
19
+ LBRACE_PATTERN = %r{\{}
20
+
21
+ def initialize(string)
22
+ @scanner = StringScanner.new(string)
23
+ @results = []
24
+ @interp_stack = []
25
+ @segment = []
26
+ end
27
+
28
+ def parse
29
+ @segment_type = :STRING
30
+
31
+ until scanner.eos?
32
+ if scanner.match?(START_INTERP_PATTERN)
33
+ start_interp
34
+ elsif !interp_stack.empty? && scanner.match?(LBRACE_PATTERN)
35
+ read_char
36
+ elsif scanner.match?(END_INTERP_PATTERN)
37
+ end_interp
38
+ elsif unenclosed_variable?
39
+ unenclosed_variable
40
+ elsif scanner.match?(ESC_DQUOTE_PATTERN)
41
+ @segment << scanner.scan(ESC_DQUOTE_PATTERN)
42
+ elsif scanner.match?(END_STRING_PATTERN)
43
+ end_string
44
+ break if interp_stack.empty?
45
+ else
46
+ read_char
47
+ end
48
+ end
49
+
50
+ raise UnterminatedStringError if results.empty? && scanner.matched?
51
+
52
+ results
53
+ end
54
+
55
+ def unenclosed_variable?
56
+ interp_stack.empty? &&
57
+ scanner.match?(UNENC_VAR_PATTERN) &&
58
+ (@segment.last.nil? ? true : !@segment.last.end_with?('\\'))
59
+ end
60
+
61
+ def parse_heredoc(heredoc_tag)
62
+ heredoc_name = heredoc_tag[%r{\A"?(.+?)"?(:.+?)?#{PuppetLint::Lexer::WHITESPACE_RE}*(/.*)?\Z}, 1]
63
+ end_heredoc_pattern = %r{\A\|?\s*-?\s*#{Regexp.escape(heredoc_name)}}
64
+ interpolation = heredoc_tag.start_with?('"')
65
+
66
+ @segment_type = :HEREDOC
67
+
68
+ until scanner.eos?
69
+ if scanner.match?(end_heredoc_pattern)
70
+ end_heredoc(end_heredoc_pattern)
71
+ break if interp_stack.empty?
72
+ elsif interpolation && scanner.match?(START_INTERP_PATTERN)
73
+ start_interp
74
+ elsif interpolation && !interp_stack.empty? && scanner.match?(LBRACE_PATTERN)
75
+ read_char
76
+ elsif interpolation && unenclosed_variable?
77
+ unenclosed_variable
78
+ elsif interpolation && scanner.match?(END_INTERP_PATTERN)
79
+ end_interp
80
+ else
81
+ read_char
82
+ end
83
+ end
84
+
85
+ results
86
+ end
87
+
88
+ def read_char
89
+ @segment << scanner.getch
90
+
91
+ return if interp_stack.empty?
92
+
93
+ case @segment.last
94
+ when '{'
95
+ interp_stack.push(true)
96
+ when '}'
97
+ interp_stack.pop
98
+ end
99
+ end
100
+
101
+ def consumed_bytes
102
+ scanner.pos
103
+ end
104
+
105
+ def start_interp
106
+ if interp_stack.empty?
107
+ scanner.skip(START_INTERP_PATTERN)
108
+ results << [@segment_type, @segment.join]
109
+ @segment = []
110
+ else
111
+ @segment << scanner.scan(START_INTERP_PATTERN)
112
+ end
113
+
114
+ interp_stack.push(true)
115
+ end
116
+
117
+ def end_interp
118
+ if interp_stack.empty?
119
+ @segment << scanner.scan(END_INTERP_PATTERN)
120
+ return
121
+ else
122
+ interp_stack.pop
123
+ end
124
+
125
+ if interp_stack.empty?
126
+ results << [:INTERP, @segment.join]
127
+ @segment = []
128
+ scanner.skip(END_INTERP_PATTERN)
129
+ else
130
+ @segment << scanner.scan(END_INTERP_PATTERN)
131
+ end
132
+ end
133
+
134
+ def unenclosed_variable
135
+ read_char if scanner.match?(%r{.\$})
136
+
137
+ results << [@segment_type, @segment.join]
138
+ results << [:UNENC_VAR, scanner.scan(UNENC_VAR_PATTERN)]
139
+ @segment = []
140
+ end
141
+
142
+ def end_heredoc(pattern)
143
+ results << [:HEREDOC, @segment.join]
144
+ results << [:HEREDOC_TERM, scanner.scan(pattern)]
145
+ end
146
+
147
+ def end_string
148
+ if interp_stack.empty?
149
+ @segment << scanner.scan(END_STRING_PATTERN).gsub!(%r{"\Z}, '')
150
+ results << [@segment_type, @segment.join]
151
+ else
152
+ @segment << scanner.scan(END_STRING_PATTERN)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -193,6 +193,8 @@ class PuppetLint
193
193
  token_iter = token_iter.send("#{direction}_token_of".to_sym, ["#{closing_token}PAREN".to_sym, opts])
194
194
  end
195
195
  end
196
+
197
+ return nil if token_iter.nil?
196
198
  token_iter = token_iter.send("#{direction}_token".to_sym)
197
199
  end
198
200
  nil
@@ -18,7 +18,6 @@ class PuppetLint::OptParser
18
18
  #
19
19
  # Returns an OptionParser object.
20
20
  def self.build(args = [])
21
- noconfig = false
22
21
  opt_parser = OptionParser.new do |opts|
23
22
  opts.banner = HELP_TEXT
24
23
 
@@ -27,7 +26,7 @@ class PuppetLint::OptParser
27
26
  end
28
27
 
29
28
  opts.on('--no-config', 'Do not load default puppet-lint option files.') do
30
- noconfig = true
29
+ # nothing to do, option is handled differently
31
30
  end
32
31
 
33
32
  opts.on('-c', '--config FILE', 'Load puppet-lint options from file.') do |file|
@@ -134,9 +133,7 @@ class PuppetLint::OptParser
134
133
  end
135
134
  end
136
135
 
137
- opt_parser.parse!(args) unless args.empty?
138
-
139
- unless noconfig
136
+ unless args.include?('--no-config')
140
137
  opt_parser.load('/etc/puppet-lint.rc')
141
138
  if ENV.key?('HOME') && File.readable?(ENV['HOME'])
142
139
  home_dotfile_path = File.expand_path('~/.puppet-lint.rc')
@@ -145,6 +142,8 @@ class PuppetLint::OptParser
145
142
  opt_parser.load('.puppet-lint.rc')
146
143
  end
147
144
 
145
+ opt_parser.parse!(args) unless args.empty?
146
+
148
147
  opt_parser
149
148
  end
150
149
  end
@@ -31,7 +31,21 @@ PuppetLint.new_check(:case_without_default) do
31
31
  case_tokens -= tokens[successor_kase[:start]..successor_kase[:end]]
32
32
  end
33
33
 
34
- next if case_tokens.index { |r| r.type == :DEFAULT }
34
+ found_matching_default = false
35
+ depth = 0
36
+
37
+ case_tokens.each do |token|
38
+ if token.type == :LBRACE
39
+ depth += 1
40
+ elsif token.type == :RBRACE
41
+ depth -= 1
42
+ elsif token.type == :DEFAULT && depth == 1
43
+ found_matching_default = true
44
+ break
45
+ end
46
+ end
47
+
48
+ next if found_matching_default
35
49
 
36
50
  notify(
37
51
  :warning,
@@ -29,9 +29,12 @@ PuppetLint.new_check(:ensure_first_param) do
29
29
  first_param_name_token = tokens[problem[:resource][:start]].next_token_of(:NAME)
30
30
  first_param_comma_token = first_param_name_token.next_token_of(:COMMA)
31
31
  ensure_param_name_token = first_param_comma_token.next_token_of(:NAME, :value => 'ensure')
32
+
33
+ raise PuppetLint::NoFix if ensure_param_name_token.nil?
34
+
32
35
  ensure_param_comma_token = ensure_param_name_token.next_token_of([:COMMA, :SEMIC])
33
36
 
34
- if first_param_name_token.nil? || first_param_comma_token.nil? || ensure_param_name_token.nil? || ensure_param_comma_token.nil?
37
+ if first_param_name_token.nil? || first_param_comma_token.nil? || ensure_param_comma_token.nil?
35
38
  raise PuppetLint::NoFix
36
39
  end
37
40
 
@@ -25,3 +25,4 @@ PuppetLint.new_check(:quoted_booleans) do
25
25
  problem[:token].type = problem[:token].value.upcase.to_sym
26
26
  end
27
27
  end
28
+ PuppetLint.configuration.send('disable_quoted_booleans')
@@ -1,9 +1,19 @@
1
+ require 'set'
2
+ require 'strscan'
3
+
1
4
  # Public: Check the manifest tokens for any variables in a string that have
2
5
  # not been enclosed by braces ({}) and record a warning for each instance
3
6
  # found.
4
7
  #
5
8
  # https://puppet.com/docs/puppet/latest/style_guide.html#quoting
6
9
  PuppetLint.new_check(:variables_not_enclosed) do
10
+ STRING_TOKEN_TYPES = Set[
11
+ :DQMID,
12
+ :DQPOST,
13
+ :HEREDOC_MID,
14
+ :HEREDOC_POST,
15
+ ]
16
+
7
17
  def check
8
18
  tokens.select { |r|
9
19
  r.type == :UNENC_VARIABLE
@@ -18,7 +28,68 @@ PuppetLint.new_check(:variables_not_enclosed) do
18
28
  end
19
29
  end
20
30
 
31
+ def hash_or_array_ref?(token)
32
+ token.next_token &&
33
+ STRING_TOKEN_TYPES.include?(token.next_token.type) &&
34
+ token.next_token.value.start_with?('[')
35
+ end
36
+
37
+ def extract_hash_or_array_ref(token)
38
+ scanner = StringScanner.new(token.value)
39
+
40
+ brack_depth = 0
41
+ result = { :ref => '' }
42
+
43
+ until scanner.eos?
44
+ result[:ref] += scanner.getch
45
+
46
+ # Pass a length of 1 when slicing the last character from the string
47
+ # to prevent Ruby 1.8 returning a Fixnum instead of a String.
48
+ case result[:ref][-1, 1]
49
+ when '['
50
+ brack_depth += 1
51
+ when ']'
52
+ brack_depth -= 1
53
+ end
54
+
55
+ break if brack_depth.zero? && scanner.peek(1) != '['
56
+ end
57
+
58
+ result[:remainder] = scanner.rest
59
+ result
60
+ end
61
+
62
+ def variable_contains_dash?(token)
63
+ token.value.include?('-')
64
+ end
65
+
66
+ def handle_variable_containing_dash(var_token)
67
+ str_token = var_token.next_token
68
+
69
+ var_name, text = var_token.value.split('-', 2)
70
+ var_token.value = var_name
71
+
72
+ return if str_token.nil?
73
+ str_token.value = "-#{text}#{str_token.value}"
74
+ end
75
+
21
76
  def fix(problem)
22
77
  problem[:token].type = :VARIABLE
78
+
79
+ if hash_or_array_ref?(problem[:token])
80
+ string_token = problem[:token].next_token
81
+ tokens_index = tokens.index(string_token)
82
+
83
+ hash_or_array_ref = extract_hash_or_array_ref(string_token)
84
+
85
+ ref_tokens = PuppetLint::Lexer.new.tokenise(hash_or_array_ref[:ref])
86
+ ref_tokens.each_with_index do |token, i|
87
+ add_token(tokens_index + i, token)
88
+ end
89
+
90
+ string_token.value = hash_or_array_ref[:remainder]
91
+ end
92
+
93
+ handle_variable_containing_dash(problem[:token]) if variable_contains_dash?(problem[:token])
23
94
  end
24
95
  end
@@ -51,7 +51,7 @@ PuppetLint.new_check(:arrow_alignment) do
51
51
  this_arrow_column = param_column[level_idx] + param_length + 1
52
52
  else
53
53
  this_arrow_column = param_token.column + param_token.to_manifest.length
54
- this_arrow_column += 1 unless param_token.type == :DQPOST
54
+ this_arrow_column += 1
55
55
  end
56
56
 
57
57
  if arrow_column[level_idx] < this_arrow_column
@@ -1,3 +1,3 @@
1
1
  class PuppetLint
2
- VERSION = '2.3.6'.freeze
2
+ VERSION = '2.4.0'.freeze
3
3
  end
@@ -0,0 +1,5 @@
1
+ # foo
2
+ define test::two_warnings() {
3
+ $var1-with-dash = 42
4
+ $VarUpperCase = false
5
+ }
@@ -526,4 +526,45 @@ describe PuppetLint::Bin do
526
526
  end
527
527
  end
528
528
  end
529
+
530
+ context 'when overriding config file options with command line options' do
531
+ context 'and config file sets "--only-checks=variable_contains_dash"' do
532
+ around(:context) do |example|
533
+ Dir.mktmpdir do |tmpdir|
534
+ Dir.chdir(tmpdir) do
535
+ File.open('.puppet-lint.rc', 'wb') do |f|
536
+ f.puts('--only-checks=variable_contains_dash')
537
+ end
538
+
539
+ example.run
540
+ end
541
+ end
542
+ end
543
+
544
+ context 'and command-line does not override "--only-checks"' do
545
+ let(:args) do
546
+ File.join(File.dirname(__FILE__), '..', 'fixtures', 'test', 'manifests', 'two_warnings.pp')
547
+ end
548
+
549
+ its(:exitstatus) { is_expected.to eq(0) }
550
+ its(:stdout) do
551
+ is_expected.to eq('WARNING: variable contains a dash on line 3')
552
+ end
553
+ end
554
+
555
+ context 'and command-line sets "--only-checks=variable_is_lowercase"' do
556
+ let(:args) do
557
+ [
558
+ '--only-checks=variable_is_lowercase',
559
+ File.join(File.dirname(__FILE__), '..', 'fixtures', 'test', 'manifests', 'two_warnings.pp'),
560
+ ]
561
+ end
562
+
563
+ its(:exitstatus) { is_expected.to eq(0) }
564
+ its(:stdout) do
565
+ is_expected.to eq('WARNING: variable contains an uppercase letter on line 4')
566
+ end
567
+ end
568
+ end
569
+ end
529
570
  end
@@ -20,6 +20,18 @@ describe PuppetLint::Data do
20
20
  }
21
21
  end
22
22
  end
23
+
24
+ context 'when typo in namespace separator makes parser look for resource' do
25
+ let(:manifest) { '$testparam = $::module:;testparam' }
26
+
27
+ it 'raises a SyntaxError' do
28
+ expect {
29
+ data.resource_indexes
30
+ }.to raise_error(PuppetLint::SyntaxError) { |error|
31
+ expect(error.token).to eq(data.tokens[5])
32
+ }
33
+ end
34
+ end
23
35
  end
24
36
 
25
37
  describe '.insert' do
@@ -0,0 +1,407 @@
1
+ require 'spec_helper'
2
+
3
+ describe PuppetLint::Lexer::StringSlurper do
4
+ describe '#parse' do
5
+ subject(:segments) { described_class.new(string).parse }
6
+
7
+ context 'when parsing an unterminated string' do
8
+ let(:string) { 'foo' }
9
+
10
+ it 'raises an UnterminatedStringError' do
11
+ expect { segments }.to raise_error(described_class::UnterminatedStringError)
12
+ end
13
+ end
14
+
15
+ context 'when parsing up to a double quote' do
16
+ let(:string) { 'foo"bar' }
17
+
18
+ it 'returns a single segment up to the double quote' do
19
+ expect(segments).to eq([[:STRING, 'foo']])
20
+ end
21
+
22
+ context 'and the string is empty' do
23
+ let(:string) { '"' }
24
+
25
+ it 'returns a single empty string segment' do
26
+ expect(segments).to eq([[:STRING, '']])
27
+ end
28
+ end
29
+
30
+ context 'and the string contains' do
31
+ context 'a newline' do
32
+ let(:string) { %(foo\nbar") }
33
+
34
+ it 'includes the newline in the string segment' do
35
+ expect(segments).to eq([[:STRING, "foo\nbar"]])
36
+ end
37
+ end
38
+
39
+ context 'an escaped $' do
40
+ let(:string) { '\$foo"' }
41
+
42
+ it 'does not create an interpolation segment' do
43
+ expect(segments).to eq([[:STRING, '\$foo']])
44
+ end
45
+ end
46
+
47
+ context 'a variable and a suffix' do
48
+ let(:string) { '${foo}bar"' }
49
+
50
+ it 'puts the variable into an interpolation segment' do
51
+ expect(segments).to eq([
52
+ [:STRING, ''],
53
+ [:INTERP, 'foo'],
54
+ [:STRING, 'bar'],
55
+ ])
56
+ end
57
+ end
58
+
59
+ context 'a variable surrounded by text' do
60
+ let(:string) { 'foo${bar}baz"' }
61
+
62
+ it 'puts the variable into an interpolation segment' do
63
+ expect(segments).to eq([
64
+ [:STRING, 'foo'],
65
+ [:INTERP, 'bar'],
66
+ [:STRING, 'baz'],
67
+ ])
68
+ end
69
+ end
70
+
71
+ context 'multiple variables with surrounding text' do
72
+ let(:string) { 'foo${bar}baz${gronk}meh"' }
73
+
74
+ it 'puts each variable into an interpolation segment' do
75
+ expect(segments).to eq([
76
+ [:STRING, 'foo'],
77
+ [:INTERP, 'bar'],
78
+ [:STRING, 'baz'],
79
+ [:INTERP, 'gronk'],
80
+ [:STRING, 'meh'],
81
+ ])
82
+ end
83
+ end
84
+
85
+ context 'only an enclosed variable' do
86
+ let(:string) { '${bar}"' }
87
+
88
+ it 'puts empty string segments around the interpolated segment' do
89
+ expect(segments).to eq([
90
+ [:STRING, ''],
91
+ [:INTERP, 'bar'],
92
+ [:STRING, ''],
93
+ ])
94
+ end
95
+ end
96
+
97
+ context 'an enclosed variable with an unnecessary $' do
98
+ let(:string) { '${$bar}"' }
99
+
100
+ it 'does not remove the unnecessary $' do
101
+ expect(segments).to eq([
102
+ [:STRING, ''],
103
+ [:INTERP, '$bar'],
104
+ [:STRING, ''],
105
+ ])
106
+ end
107
+ end
108
+
109
+ context 'a variable with an array reference' do
110
+ let(:string) { '${foo[bar][baz]}"' }
111
+
112
+ it 'includes the references in the interpolated section' do
113
+ expect(segments).to eq([
114
+ [:STRING, ''],
115
+ [:INTERP, 'foo[bar][baz]'],
116
+ [:STRING, ''],
117
+ ])
118
+ end
119
+ end
120
+
121
+ context 'only enclosed variables' do
122
+ let(:string) { '${foo}${bar}"' }
123
+
124
+ it 'creates an interpolation section per variable' do
125
+ expect(segments).to eq([
126
+ [:STRING, ''],
127
+ [:INTERP, 'foo'],
128
+ [:STRING, ''],
129
+ [:INTERP, 'bar'],
130
+ [:STRING, ''],
131
+ ])
132
+ end
133
+ end
134
+
135
+ context 'an unenclosed variable' do
136
+ let(:string) { '$foo"' }
137
+
138
+ it 'creates a special segment for the unenclosed variable' do
139
+ expect(segments).to eq([
140
+ [:STRING, ''],
141
+ [:UNENC_VAR, '$foo'],
142
+ [:STRING, ''],
143
+ ])
144
+ end
145
+ end
146
+
147
+ context 'an interpolation with a nested single quoted string' do
148
+ let(:string) { %(string with ${'a nested single quoted string'} inside it") }
149
+
150
+ it 'creates an interpolation segment for the nested string' do
151
+ expect(segments).to eq([
152
+ [:STRING, 'string with '],
153
+ [:INTERP, "'a nested single quoted string'"],
154
+ [:STRING, ' inside it'],
155
+ ])
156
+ end
157
+ end
158
+
159
+ context 'an interpolation with nested math' do
160
+ let(:string) { 'string with ${(3+5)/4} nested math"' }
161
+
162
+ it 'creates an interpolation segment for the nested math' do
163
+ expect(segments).to eq([
164
+ [:STRING, 'string with '],
165
+ [:INTERP, '(3+5)/4'],
166
+ [:STRING, ' nested math'],
167
+ ])
168
+ end
169
+ end
170
+
171
+ context 'an interpolation with a nested array' do
172
+ let(:string) { %(string with ${['an array ', $v2]} in it") }
173
+
174
+ it 'creates an interpolation segment for the nested array' do
175
+ expect(segments).to eq([
176
+ [:STRING, 'string with '],
177
+ [:INTERP, "['an array ', $v2]"],
178
+ [:STRING, ' in it'],
179
+ ])
180
+ end
181
+ end
182
+
183
+ context 'repeated $s' do
184
+ let(:string) { '$$$$"' }
185
+
186
+ it 'creates a single string segment' do
187
+ expect(segments).to eq([[:STRING, '$$$$']])
188
+ end
189
+ end
190
+
191
+ context 'multiple unenclosed variables' do
192
+ let(:string) { '$foo$bar"' }
193
+
194
+ it 'creates a special segment for each unenclosed variable' do
195
+ expect(segments).to eq([
196
+ [:STRING, ''],
197
+ [:UNENC_VAR, '$foo'],
198
+ [:STRING, ''],
199
+ [:UNENC_VAR, '$bar'],
200
+ [:STRING, ''],
201
+ ])
202
+ end
203
+ end
204
+
205
+ context 'an unenclosed variable with a trailing $' do
206
+ let(:string) { 'foo$bar$"' }
207
+
208
+ it 'places the trailing $ in a string segment' do
209
+ expect(segments).to eq([
210
+ [:STRING, 'foo'],
211
+ [:UNENC_VAR, '$bar'],
212
+ [:STRING, '$'],
213
+ ])
214
+ end
215
+ end
216
+
217
+ context 'an unenclosed variable starting with two $s' do
218
+ let(:string) { 'foo$$bar"' }
219
+
220
+ it 'includes the preceeding $ in the string segment before the unenclosed variable' do
221
+ expect(segments).to eq([
222
+ [:STRING, 'foo$'],
223
+ [:UNENC_VAR, '$bar'],
224
+ [:STRING, ''],
225
+ ])
226
+ end
227
+ end
228
+
229
+ context 'an unenclosed variable with incorrect namespacing' do
230
+ let(:string) { '$foo::::bar"' }
231
+
232
+ it 'only includes the valid part of the variable name in the segment' do
233
+ expect(segments).to eq([
234
+ [:STRING, ''],
235
+ [:UNENC_VAR, '$foo'],
236
+ [:STRING, '::::bar'],
237
+ ])
238
+ end
239
+ end
240
+
241
+ context 'an interpolation with a complex function chain' do
242
+ let(:string) { '${key} ${flatten([$value]).join("\nkey ")}"' }
243
+
244
+ it 'keeps the whole function chain in a single interpolation segment' do
245
+ expect(segments).to eq([
246
+ [:STRING, ''],
247
+ [:INTERP, 'key'],
248
+ [:STRING, ' '],
249
+ [:INTERP, 'flatten([$value]).join("\nkey ")'],
250
+ [:STRING, ''],
251
+ ])
252
+ end
253
+ end
254
+
255
+ context 'nested interpolations' do
256
+ let(:string) { '${facts["network_${iface}"]}/${facts["netmask_${iface}"]}"' }
257
+
258
+ it 'keeps each full interpolation in its own segment' do
259
+ expect(segments).to eq([
260
+ [:STRING, ''],
261
+ [:INTERP, 'facts["network_${iface}"]'],
262
+ [:STRING, '/'],
263
+ [:INTERP, 'facts["netmask_${iface}"]'],
264
+ [:STRING, ''],
265
+ ])
266
+ end
267
+ end
268
+
269
+ context 'interpolation with nested braces' do
270
+ let(:string) { '${$foo.map |$bar| { something($bar) }}"' }
271
+
272
+ it do
273
+ expect(segments).to eq([
274
+ [:STRING, ''],
275
+ [:INTERP, '$foo.map |$bar| { something($bar) }'],
276
+ [:STRING, ''],
277
+ ])
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
283
+
284
+ describe '#parse_heredoc' do
285
+ subject(:segments) { described_class.new(heredoc).parse_heredoc(heredoc_tag) }
286
+
287
+ context 'when parsing a heredoc with interpolation disabled' do
288
+ context 'that is a plain heredoc' do
289
+ let(:heredoc) { %( SOMETHING\n ELSE\n :\n |-myheredoc) }
290
+ let(:heredoc_tag) { 'myheredoc' }
291
+
292
+ it 'splits the heredoc into two segments' do
293
+ expect(segments).to eq([
294
+ [:HEREDOC, " SOMETHING\n ELSE\n :\n "],
295
+ [:HEREDOC_TERM, '|-myheredoc'],
296
+ ])
297
+ end
298
+ end
299
+
300
+ context 'that contains a value enclosed in ${}' do
301
+ let(:heredoc) { %( SOMETHING\n ${else}\n :\n |-myheredoc) }
302
+ let(:heredoc_tag) { 'myheredoc' }
303
+
304
+ it 'does not create an interpolation segment' do
305
+ expect(segments).to eq([
306
+ [:HEREDOC, " SOMETHING\n ${else}\n :\n "],
307
+ [:HEREDOC_TERM, '|-myheredoc'],
308
+ ])
309
+ end
310
+ end
311
+
312
+ context 'that contains an unenclosed variable' do
313
+ let(:heredoc) { %( SOMETHING\n $else\n :\n |-myheredoc) }
314
+ let(:heredoc_tag) { 'myheredoc' }
315
+
316
+ it 'does not create a segment for the unenclosed variable' do
317
+ expect(segments).to eq([
318
+ [:HEREDOC, " SOMETHING\n $else\n :\n "],
319
+ [:HEREDOC_TERM, '|-myheredoc'],
320
+ ])
321
+ end
322
+ end
323
+ end
324
+
325
+ context 'when parsing a heredoc with interpolation enabled' do
326
+ context 'that is a plain heredoc' do
327
+ let(:heredoc) { %( SOMETHING\n ELSE\n :\n |-myheredoc) }
328
+ let(:heredoc_tag) { '"myheredoc"' }
329
+
330
+ it 'splits the heredoc into two segments' do
331
+ expect(segments).to eq([
332
+ [:HEREDOC, " SOMETHING\n ELSE\n :\n "],
333
+ [:HEREDOC_TERM, '|-myheredoc'],
334
+ ])
335
+ end
336
+ end
337
+
338
+ context 'that contains a value enclosed in ${}' do
339
+ let(:heredoc) { %( SOMETHING\n ${else}\n :\n |-myheredoc) }
340
+ let(:heredoc_tag) { '"myheredoc"' }
341
+
342
+ it 'creates an interpolation segment' do
343
+ expect(segments).to eq([
344
+ [:HEREDOC, " SOMETHING\n "],
345
+ [:INTERP, 'else'],
346
+ [:HEREDOC, "\n :\n "],
347
+ [:HEREDOC_TERM, '|-myheredoc'],
348
+ ])
349
+ end
350
+ end
351
+
352
+ context 'that contains an unenclosed variable' do
353
+ let(:heredoc) { %( SOMETHING\n $else\n :\n |-myheredoc) }
354
+ let(:heredoc_tag) { '"myheredoc"' }
355
+
356
+ it 'does not create a segment for the unenclosed variable' do
357
+ expect(segments).to eq([
358
+ [:HEREDOC, " SOMETHING\n "],
359
+ [:UNENC_VAR, '$else'],
360
+ [:HEREDOC, "\n :\n "],
361
+ [:HEREDOC_TERM, '|-myheredoc'],
362
+ ])
363
+ end
364
+ end
365
+
366
+ context 'that contains a nested interpolation' do
367
+ let(:heredoc) { %( SOMETHING\n ${facts["other_${thing}"]}\n :\n |-myheredoc) }
368
+ let(:heredoc_tag) { '"myheredoc"' }
369
+
370
+ it 'does not create a segment for the unenclosed variable' do
371
+ expect(segments).to eq([
372
+ [:HEREDOC, " SOMETHING\n "],
373
+ [:INTERP, 'facts["other_${thing}"]'],
374
+ [:HEREDOC, "\n :\n "],
375
+ [:HEREDOC_TERM, '|-myheredoc'],
376
+ ])
377
+ end
378
+ end
379
+
380
+ context 'that contains an interpolation with nested braces' do
381
+ let(:heredoc) { %( SOMETHING\n ${$foo.map |$bar| { something($bar) }}\n :\n |-myheredoc) }
382
+ let(:heredoc_tag) { '"myheredoc"' }
383
+
384
+ it 'does not create a segment for the unenclosed variable' do
385
+ expect(segments).to eq([
386
+ [:HEREDOC, " SOMETHING\n "],
387
+ [:INTERP, '$foo.map |$bar| { something($bar) }'],
388
+ [:HEREDOC, "\n :\n "],
389
+ [:HEREDOC_TERM, '|-myheredoc'],
390
+ ])
391
+ end
392
+ end
393
+
394
+ context 'that contains braces' do
395
+ let(:heredoc) { %( {\n "foo": "bar"\n }\n |-end) }
396
+ let(:heredoc_tag) { '"end":json/' }
397
+
398
+ it do
399
+ expect(segments).to eq([
400
+ [:HEREDOC, %( {\n "foo": "bar"\n }\n )],
401
+ [:HEREDOC_TERM, '|-end'],
402
+ ])
403
+ end
404
+ end
405
+ end
406
+ end
407
+ end