puppet-lint 2.3.6 → 2.5.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.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +238 -87
  3. data/README.md +18 -0
  4. data/lib/puppet-lint.rb +1 -1
  5. data/lib/puppet-lint/data.rb +26 -11
  6. data/lib/puppet-lint/lexer.rb +97 -200
  7. data/lib/puppet-lint/lexer/string_slurper.rb +173 -0
  8. data/lib/puppet-lint/lexer/token.rb +8 -0
  9. data/lib/puppet-lint/optparser.rb +4 -5
  10. data/lib/puppet-lint/plugins/check_classes/parameter_order.rb +12 -1
  11. data/lib/puppet-lint/plugins/check_conditionals/case_without_default.rb +15 -1
  12. data/lib/puppet-lint/plugins/check_documentation/documentation.rb +4 -0
  13. data/lib/puppet-lint/plugins/check_resources/ensure_first_param.rb +5 -2
  14. data/lib/puppet-lint/plugins/check_strings/quoted_booleans.rb +1 -0
  15. data/lib/puppet-lint/plugins/check_strings/variables_not_enclosed.rb +71 -0
  16. data/lib/puppet-lint/plugins/check_whitespace/arrow_alignment.rb +1 -1
  17. data/lib/puppet-lint/tasks/puppet-lint.rb +14 -0
  18. data/lib/puppet-lint/tasks/release_test.rb +3 -1
  19. data/lib/puppet-lint/version.rb +1 -1
  20. data/spec/fixtures/test/manifests/two_warnings.pp +5 -0
  21. data/spec/puppet-lint/bin_spec.rb +47 -6
  22. data/spec/puppet-lint/data_spec.rb +12 -0
  23. data/spec/puppet-lint/lexer/string_slurper_spec.rb +473 -0
  24. data/spec/puppet-lint/lexer_spec.rb +1153 -590
  25. data/spec/puppet-lint/plugins/check_classes/parameter_order_spec.rb +18 -0
  26. data/spec/puppet-lint/plugins/check_classes/variable_scope_spec.rb +15 -1
  27. data/spec/puppet-lint/plugins/check_conditionals/case_without_default_spec.rb +39 -0
  28. data/spec/puppet-lint/plugins/check_documentation/documentation_spec.rb +18 -0
  29. data/spec/puppet-lint/plugins/check_resources/ensure_first_param_spec.rb +16 -0
  30. data/spec/puppet-lint/plugins/check_strings/double_quoted_strings_spec.rb +5 -5
  31. data/spec/puppet-lint/plugins/check_strings/only_variable_string_spec.rb +6 -6
  32. data/spec/puppet-lint/plugins/check_strings/variables_not_enclosed_spec.rb +32 -0
  33. data/spec/puppet-lint/plugins/check_variables/variable_is_lowercase_spec.rb +28 -0
  34. data/spec/spec_helper.rb +7 -5
  35. metadata +14 -17
  36. data/.gitignore +0 -12
  37. data/.rspec +0 -2
  38. data/.rubocop.yml +0 -74
  39. data/.rubocop_todo.yml +0 -89
  40. data/.travis.yml +0 -24
  41. data/Gemfile +0 -40
  42. data/Rakefile +0 -42
  43. data/appveyor.yml +0 -33
  44. data/puppet-lint.gemspec +0 -19
@@ -55,7 +55,9 @@ def with_puppet_lint_head
55
55
  end
56
56
 
57
57
  task :release_test do
58
- branch = if ENV['APPVEYOR']
58
+ branch = if ENV['GITHUB_REF']
59
+ ENV['GITHUB_REF']
60
+ elsif ENV['APPVEYOR']
59
61
  ENV['APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH']
60
62
  elsif ENV['TRAVIS']
61
63
  ENV['TRAVIS_PULL_REQUEST_BRANCH']
@@ -1,3 +1,3 @@
1
1
  class PuppetLint
2
- VERSION = '2.3.6'.freeze
2
+ VERSION = '2.5.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
+ }
@@ -91,8 +91,8 @@ describe PuppetLint::Bin do
91
91
  its(:stdout) do
92
92
  is_expected.to eq(
93
93
  [
94
- "#{args[0]} - WARNING: optional parameter listed before required parameter on line 2",
95
- "#{args[1]} - ERROR: test::foo not in autoload module layout on line 2",
94
+ "#{args[0]} - WARNING: optional parameter listed before required parameter on line 2 (check: parameter_order)",
95
+ "#{args[1]} - ERROR: test::foo not in autoload module layout on line 2 (check: autoloader_layout)",
96
96
  ].join("\n")
97
97
  )
98
98
  end
@@ -102,7 +102,7 @@ describe PuppetLint::Bin do
102
102
  let(:args) { 'spec/fixtures/test/manifests/malformed.pp' }
103
103
 
104
104
  its(:exitstatus) { is_expected.to eq(1) }
105
- its(:stdout) { is_expected.to eq('ERROR: Syntax error on line 1') }
105
+ its(:stdout) { is_expected.to eq('ERROR: Syntax error on line 1 (check: syntax)') }
106
106
  its(:stderr) { is_expected.to eq('Try running `puppet parser validate <file>`') }
107
107
  end
108
108
 
@@ -198,7 +198,7 @@ describe PuppetLint::Bin do
198
198
  its(:stdout) do
199
199
  is_expected.to eq(
200
200
  [
201
- 'WARNING: optional parameter listed before required parameter on line 2',
201
+ 'WARNING: optional parameter listed before required parameter on line 2 (check: parameter_order)',
202
202
  '',
203
203
  " define test::warning($foo='bar', $baz) { }",
204
204
  ' ^',
@@ -432,7 +432,7 @@ describe PuppetLint::Bin do
432
432
  its(:stdout) do
433
433
  is_expected.to eq(
434
434
  [
435
- 'IGNORED: double quoted string containing no variables on line 3',
435
+ 'IGNORED: double quoted string containing no variables on line 3 (check: double_quoted_strings)',
436
436
  ' for a good reason',
437
437
  ].join("\n")
438
438
  )
@@ -457,7 +457,7 @@ describe PuppetLint::Bin do
457
457
  end
458
458
 
459
459
  its(:exitstatus) { is_expected.to eq(0) }
460
- its(:stdout) { is_expected.to match(%r{^.*line 6$}) }
460
+ its(:stdout) { is_expected.to match(%r{^.*line 6(?!\d)}) }
461
461
  end
462
462
 
463
463
  context 'when an lint:endignore control comment exists with no opening lint:ignore comment' do
@@ -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 (check: variable_contains_dash)')
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 (check: variable_is_lowercase)')
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,473 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PuppetLint::Lexer::StringSlurper do
6
+ describe '#parse' do
7
+ subject(:segments) { described_class.new(string).parse }
8
+
9
+ context 'when parsing an unterminated string' do
10
+ let(:string) { 'foo' }
11
+
12
+ it 'raises an UnterminatedStringError' do
13
+ expect { segments }.to raise_error(described_class::UnterminatedStringError)
14
+ end
15
+ end
16
+
17
+ context 'when parsing up to a double quote' do
18
+ let(:string) { 'foo"bar' }
19
+
20
+ it 'returns a single segment up to the double quote' do
21
+ expect(segments).to eq([[:STRING, 'foo']])
22
+ end
23
+
24
+ context 'and the string is empty' do
25
+ let(:string) { '"' }
26
+
27
+ it 'returns a single empty string segment' do
28
+ expect(segments).to eq([[:STRING, '']])
29
+ end
30
+ end
31
+
32
+ context 'and the string contains' do
33
+ context 'a newline' do
34
+ let(:string) { %(foo\nbar") }
35
+
36
+ it 'includes the newline in the string segment' do
37
+ expect(segments).to eq([[:STRING, "foo\nbar"]])
38
+ end
39
+ end
40
+
41
+ context 'an escaped $var' do
42
+ let(:string) { '\$foo"' }
43
+
44
+ it 'does not create an unenclosed variable segment' do
45
+ expect(segments).to eq([[:STRING, '\$foo']])
46
+ end
47
+ end
48
+
49
+ context 'an escaped ${} enclosure' do
50
+ let(:string) { '\"\${\"string\"}\""' }
51
+
52
+ it 'does not create an interpolation segment' do
53
+ expect(segments).to eq([[:STRING, '\"\${\"string\"}\"']])
54
+ end
55
+ end
56
+
57
+ context 'a variable and a suffix' do
58
+ let(:string) { '${foo}bar"' }
59
+
60
+ it 'puts the variable into an interpolation segment' do
61
+ expect(segments).to eq([
62
+ [:STRING, ''],
63
+ [:INTERP, 'foo'],
64
+ [:STRING, 'bar'],
65
+ ])
66
+ end
67
+ end
68
+
69
+ context 'a variable surrounded by text' do
70
+ let(:string) { 'foo${bar}baz"' }
71
+
72
+ it 'puts the variable into an interpolation segment' do
73
+ expect(segments).to eq([
74
+ [:STRING, 'foo'],
75
+ [:INTERP, 'bar'],
76
+ [:STRING, 'baz'],
77
+ ])
78
+ end
79
+ end
80
+
81
+ context 'multiple variables with surrounding text' do
82
+ let(:string) { 'foo${bar}baz${gronk}meh"' }
83
+
84
+ it 'puts each variable into an interpolation segment' do
85
+ expect(segments).to eq([
86
+ [:STRING, 'foo'],
87
+ [:INTERP, 'bar'],
88
+ [:STRING, 'baz'],
89
+ [:INTERP, 'gronk'],
90
+ [:STRING, 'meh'],
91
+ ])
92
+ end
93
+ end
94
+
95
+ context 'only an enclosed variable' do
96
+ let(:string) { '${bar}"' }
97
+
98
+ it 'puts empty string segments around the interpolated segment' do
99
+ expect(segments).to eq([
100
+ [:STRING, ''],
101
+ [:INTERP, 'bar'],
102
+ [:STRING, ''],
103
+ ])
104
+ end
105
+ end
106
+
107
+ context 'an enclosed variable with an unnecessary $' do
108
+ let(:string) { '${$bar}"' }
109
+
110
+ it 'does not remove the unnecessary $' do
111
+ expect(segments).to eq([
112
+ [:STRING, ''],
113
+ [:INTERP, '$bar'],
114
+ [:STRING, ''],
115
+ ])
116
+ end
117
+ end
118
+
119
+ context 'a variable with an array reference' do
120
+ let(:string) { '${foo[bar][baz]}"' }
121
+
122
+ it 'includes the references in the interpolated section' do
123
+ expect(segments).to eq([
124
+ [:STRING, ''],
125
+ [:INTERP, 'foo[bar][baz]'],
126
+ [:STRING, ''],
127
+ ])
128
+ end
129
+ end
130
+
131
+ context 'only enclosed variables' do
132
+ let(:string) { '${foo}${bar}"' }
133
+
134
+ it 'creates an interpolation section per variable' do
135
+ expect(segments).to eq([
136
+ [:STRING, ''],
137
+ [:INTERP, 'foo'],
138
+ [:STRING, ''],
139
+ [:INTERP, 'bar'],
140
+ [:STRING, ''],
141
+ ])
142
+ end
143
+ end
144
+
145
+ context 'an unenclosed variable' do
146
+ let(:string) { '$foo"' }
147
+
148
+ it 'creates a special segment for the unenclosed variable' do
149
+ expect(segments).to eq([
150
+ [:STRING, ''],
151
+ [:UNENC_VAR, '$foo'],
152
+ [:STRING, ''],
153
+ ])
154
+ end
155
+ end
156
+
157
+ context 'an interpolation with a nested single quoted string' do
158
+ let(:string) { %(string with ${'a nested single quoted string'} inside it") }
159
+
160
+ it 'creates an interpolation segment for the nested string' do
161
+ expect(segments).to eq([
162
+ [:STRING, 'string with '],
163
+ [:INTERP, "'a nested single quoted string'"],
164
+ [:STRING, ' inside it'],
165
+ ])
166
+ end
167
+ end
168
+
169
+ context 'an interpolation with nested math' do
170
+ let(:string) { 'string with ${(3+5)/4} nested math"' }
171
+
172
+ it 'creates an interpolation segment for the nested math' do
173
+ expect(segments).to eq([
174
+ [:STRING, 'string with '],
175
+ [:INTERP, '(3+5)/4'],
176
+ [:STRING, ' nested math'],
177
+ ])
178
+ end
179
+ end
180
+
181
+ context 'an interpolation with a nested array' do
182
+ let(:string) { %(string with ${['an array ', $v2]} in it") }
183
+
184
+ it 'creates an interpolation segment for the nested array' do
185
+ expect(segments).to eq([
186
+ [:STRING, 'string with '],
187
+ [:INTERP, "['an array ', $v2]"],
188
+ [:STRING, ' in it'],
189
+ ])
190
+ end
191
+ end
192
+
193
+ context 'repeated $s' do
194
+ let(:string) { '$$$$"' }
195
+
196
+ it 'creates a single string segment' do
197
+ expect(segments).to eq([[:STRING, '$$$$']])
198
+ end
199
+ end
200
+
201
+ context 'multiple unenclosed variables' do
202
+ let(:string) { '$foo$bar"' }
203
+
204
+ it 'creates a special segment for each unenclosed variable' do
205
+ expect(segments).to eq([
206
+ [:STRING, ''],
207
+ [:UNENC_VAR, '$foo'],
208
+ [:STRING, ''],
209
+ [:UNENC_VAR, '$bar'],
210
+ [:STRING, ''],
211
+ ])
212
+ end
213
+ end
214
+
215
+ context 'an unenclosed variable with a trailing $' do
216
+ let(:string) { 'foo$bar$"' }
217
+
218
+ it 'places the trailing $ in a string segment' do
219
+ expect(segments).to eq([
220
+ [:STRING, 'foo'],
221
+ [:UNENC_VAR, '$bar'],
222
+ [:STRING, '$'],
223
+ ])
224
+ end
225
+ end
226
+
227
+ context 'an unenclosed variable starting with two $s' do
228
+ let(:string) { 'foo$$bar"' }
229
+
230
+ it 'includes the preceeding $ in the string segment before the unenclosed variable' do
231
+ expect(segments).to eq([
232
+ [:STRING, 'foo$'],
233
+ [:UNENC_VAR, '$bar'],
234
+ [:STRING, ''],
235
+ ])
236
+ end
237
+ end
238
+
239
+ context 'an unenclosed variable with incorrect namespacing' do
240
+ let(:string) { '$foo::::bar"' }
241
+
242
+ it 'only includes the valid part of the variable name in the segment' do
243
+ expect(segments).to eq([
244
+ [:STRING, ''],
245
+ [:UNENC_VAR, '$foo'],
246
+ [:STRING, '::::bar'],
247
+ ])
248
+ end
249
+ end
250
+
251
+ context 'a variable followed by an odd number of backslashes before a double quote' do
252
+ let(:string) { '${foo}\"bar"' }
253
+
254
+ it 'does not let this double quote terminate the string' do
255
+ expect(segments).to eq([
256
+ [:STRING, ''],
257
+ [:INTERP, 'foo'],
258
+ [:STRING, '\\"bar'],
259
+ ])
260
+ end
261
+ end
262
+
263
+ context 'a variable followed by an even number of backslashes before a double quote' do
264
+ let(:string) { '${foo}\\\\"bar"' }
265
+
266
+ it 'recognizes this double quote as the terminator' do
267
+ expect(segments).to eq([
268
+ [:STRING, ''],
269
+ [:INTERP, 'foo'],
270
+ [:STRING, '\\\\'],
271
+ ])
272
+ end
273
+ end
274
+
275
+ context 'an interpolation with a complex function chain' do
276
+ let(:string) { '${key} ${flatten([$value]).join("\nkey ")}"' }
277
+
278
+ it 'keeps the whole function chain in a single interpolation segment' do
279
+ expect(segments).to eq([
280
+ [:STRING, ''],
281
+ [:INTERP, 'key'],
282
+ [:STRING, ' '],
283
+ [:INTERP, 'flatten([$value]).join("\nkey ")'],
284
+ [:STRING, ''],
285
+ ])
286
+ end
287
+ end
288
+
289
+ context 'nested interpolations' do
290
+ let(:string) { '${facts["network_${iface}"]}/${facts["netmask_${iface}"]}"' }
291
+
292
+ it 'keeps each full interpolation in its own segment' do
293
+ expect(segments).to eq([
294
+ [:STRING, ''],
295
+ [:INTERP, 'facts["network_${iface}"]'],
296
+ [:STRING, '/'],
297
+ [:INTERP, 'facts["netmask_${iface}"]'],
298
+ [:STRING, ''],
299
+ ])
300
+ end
301
+ end
302
+
303
+ context 'interpolation with nested braces' do
304
+ let(:string) { '${$foo.map |$bar| { something($bar) }}"' }
305
+
306
+ it do
307
+ expect(segments).to eq([
308
+ [:STRING, ''],
309
+ [:INTERP, '$foo.map |$bar| { something($bar) }'],
310
+ [:STRING, ''],
311
+ ])
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+ describe '#parse_heredoc' do
319
+ subject(:segments) { described_class.new(heredoc).parse_heredoc(heredoc_tag) }
320
+
321
+ context 'when the heredoc text contains the tag' do
322
+ let(:heredoc) { %( SOMETHING else\n |-THING) }
323
+ let(:heredoc_tag) { 'THING' }
324
+
325
+ it 'terminates the heredoc at the closing tag' do
326
+ expect(segments).to eq([
327
+ [:HEREDOC, " SOMETHING else\n "],
328
+ [:HEREDOC_TERM, '|-THING'],
329
+ ])
330
+ end
331
+ end
332
+
333
+ context 'when parsing a heredoc with interpolation disabled' do
334
+ context 'that is a plain heredoc' do
335
+ let(:heredoc) { %( SOMETHING\n ELSE\n :\n |-myheredoc) }
336
+ let(:heredoc_tag) { 'myheredoc' }
337
+
338
+ it 'splits the heredoc into two segments' do
339
+ expect(segments).to eq([
340
+ [:HEREDOC, " SOMETHING\n ELSE\n :\n "],
341
+ [:HEREDOC_TERM, '|-myheredoc'],
342
+ ])
343
+ end
344
+ end
345
+
346
+ context 'that contains a value enclosed in ${}' do
347
+ let(:heredoc) { %( SOMETHING\n ${else}\n :\n |-myheredoc) }
348
+ let(:heredoc_tag) { 'myheredoc' }
349
+
350
+ it 'does not create an interpolation segment' do
351
+ expect(segments).to eq([
352
+ [:HEREDOC, " SOMETHING\n ${else}\n :\n "],
353
+ [:HEREDOC_TERM, '|-myheredoc'],
354
+ ])
355
+ end
356
+ end
357
+
358
+ context 'that contains an unenclosed variable' do
359
+ let(:heredoc) { %( SOMETHING\n $else\n :\n |-myheredoc) }
360
+ let(:heredoc_tag) { 'myheredoc' }
361
+
362
+ it 'does not create a segment for the unenclosed variable' do
363
+ expect(segments).to eq([
364
+ [:HEREDOC, " SOMETHING\n $else\n :\n "],
365
+ [:HEREDOC_TERM, '|-myheredoc'],
366
+ ])
367
+ end
368
+ end
369
+ end
370
+
371
+ context 'when parsing a heredoc with interpolation enabled' do
372
+ context 'that is a plain heredoc' do
373
+ let(:heredoc) { %( SOMETHING\n ELSE\n :\n |-myheredoc) }
374
+ let(:heredoc_tag) { '"myheredoc"' }
375
+
376
+ it 'splits the heredoc into two segments' do
377
+ expect(segments).to eq([
378
+ [:HEREDOC, " SOMETHING\n ELSE\n :\n "],
379
+ [:HEREDOC_TERM, '|-myheredoc'],
380
+ ])
381
+ end
382
+ end
383
+
384
+ context 'that contains a value enclosed in ${}' do
385
+ let(:heredoc) { %( SOMETHING\n ${else}\n :\n |-myheredoc) }
386
+ let(:heredoc_tag) { '"myheredoc"' }
387
+
388
+ it 'creates an interpolation segment' do
389
+ expect(segments).to eq([
390
+ [:HEREDOC, " SOMETHING\n "],
391
+ [:INTERP, 'else'],
392
+ [:HEREDOC, "\n :\n "],
393
+ [:HEREDOC_TERM, '|-myheredoc'],
394
+ ])
395
+ end
396
+ end
397
+
398
+ context 'that contains an unenclosed variable' do
399
+ let(:heredoc) { %( SOMETHING\n $else\n :\n |-myheredoc) }
400
+ let(:heredoc_tag) { '"myheredoc"' }
401
+
402
+ it 'does not create a segment for the unenclosed variable' do
403
+ expect(segments).to eq([
404
+ [:HEREDOC, " SOMETHING\n "],
405
+ [:UNENC_VAR, '$else'],
406
+ [:HEREDOC, "\n :\n "],
407
+ [:HEREDOC_TERM, '|-myheredoc'],
408
+ ])
409
+ end
410
+ end
411
+
412
+ context 'that contains a nested interpolation' do
413
+ let(:heredoc) { %( SOMETHING\n ${facts["other_${thing}"]}\n :\n |-myheredoc) }
414
+ let(:heredoc_tag) { '"myheredoc"' }
415
+
416
+ it 'does not create a segment for the unenclosed variable' do
417
+ expect(segments).to eq([
418
+ [:HEREDOC, " SOMETHING\n "],
419
+ [:INTERP, 'facts["other_${thing}"]'],
420
+ [:HEREDOC, "\n :\n "],
421
+ [:HEREDOC_TERM, '|-myheredoc'],
422
+ ])
423
+ end
424
+ end
425
+
426
+ context 'that contains an interpolation with nested braces' do
427
+ let(:heredoc) { %( SOMETHING\n ${$foo.map |$bar| { something($bar) }}\n :\n |-myheredoc) }
428
+ let(:heredoc_tag) { '"myheredoc"' }
429
+
430
+ it 'does not create a segment for the unenclosed variable' do
431
+ expect(segments).to eq([
432
+ [:HEREDOC, " SOMETHING\n "],
433
+ [:INTERP, '$foo.map |$bar| { something($bar) }'],
434
+ [:HEREDOC, "\n :\n "],
435
+ [:HEREDOC_TERM, '|-myheredoc'],
436
+ ])
437
+ end
438
+ end
439
+
440
+ context 'that contains braces' do
441
+ let(:heredoc) { %( {\n "foo": "bar"\n }\n |-end) }
442
+ let(:heredoc_tag) { '"end":json/' }
443
+
444
+ it do
445
+ expect(segments).to eq([
446
+ [:HEREDOC, %( {\n "foo": "bar"\n }\n )],
447
+ [:HEREDOC_TERM, '|-end'],
448
+ ])
449
+ end
450
+ end
451
+ end
452
+ end
453
+
454
+ describe '#consumed_chars' do
455
+ subject { described_class.new(string).tap(&:parse).consumed_chars }
456
+
457
+ context 'when slurping a string containing multibyte characters' do
458
+ let(:string) { 'accentués"' }
459
+
460
+ it 'counts the multibyte character as a single consumed character' do
461
+ is_expected.to eq(10)
462
+ end
463
+ end
464
+
465
+ context 'when slurping an empty string' do
466
+ let(:string) { '"' }
467
+
468
+ it 'consumes only the closing quote' do
469
+ is_expected.to eq(1)
470
+ end
471
+ end
472
+ end
473
+ end