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.
- checksums.yaml +5 -5
- data/.rubocop_todo.yml +2 -2
- data/.travis.yml +7 -5
- data/CHANGELOG.md +53 -0
- data/README.md +18 -0
- data/appveyor.yml +2 -0
- data/lib/puppet-lint/data.rb +17 -2
- data/lib/puppet-lint/lexer.rb +90 -204
- data/lib/puppet-lint/lexer/string_slurper.rb +157 -0
- data/lib/puppet-lint/lexer/token.rb +2 -0
- data/lib/puppet-lint/optparser.rb +4 -5
- data/lib/puppet-lint/plugins/check_conditionals/case_without_default.rb +15 -1
- data/lib/puppet-lint/plugins/check_resources/ensure_first_param.rb +4 -1
- data/lib/puppet-lint/plugins/check_strings/quoted_booleans.rb +1 -0
- data/lib/puppet-lint/plugins/check_strings/variables_not_enclosed.rb +71 -0
- data/lib/puppet-lint/plugins/check_whitespace/arrow_alignment.rb +1 -1
- data/lib/puppet-lint/version.rb +1 -1
- data/spec/fixtures/test/manifests/two_warnings.pp +5 -0
- data/spec/puppet-lint/bin_spec.rb +41 -0
- data/spec/puppet-lint/data_spec.rb +12 -0
- data/spec/puppet-lint/lexer/string_slurper_spec.rb +407 -0
- data/spec/puppet-lint/lexer_spec.rb +1097 -590
- data/spec/puppet-lint/plugins/check_classes/variable_scope_spec.rb +1 -1
- data/spec/puppet-lint/plugins/check_conditionals/case_without_default_spec.rb +39 -0
- data/spec/puppet-lint/plugins/check_resources/ensure_first_param_spec.rb +16 -0
- data/spec/puppet-lint/plugins/check_strings/only_variable_string_spec.rb +6 -6
- data/spec/puppet-lint/plugins/check_strings/variables_not_enclosed_spec.rb +32 -0
- metadata +8 -4
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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? ||
|
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
|
|
@@ -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
|
54
|
+
this_arrow_column += 1
|
55
55
|
end
|
56
56
|
|
57
57
|
if arrow_column[level_idx] < this_arrow_column
|
data/lib/puppet-lint/version.rb
CHANGED
@@ -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
|