puppet-lint 2.3.6 → 2.5.0

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