puppet-lint 2.3.6 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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