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.
- 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
|