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
@@ -0,0 +1,173 @@
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?(END_STRING_PATTERN)
41
+ end_string
42
+ break if interp_stack.empty?
43
+ elsif scanner.match?(ESC_DQUOTE_PATTERN)
44
+ @segment << scanner.scan(ESC_DQUOTE_PATTERN)
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{^\|?\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
+ # Get the number of characters consumed by the StringSlurper.
102
+ #
103
+ # StringScanner from Ruby 2.0 onwards supports #charpos which returns
104
+ # the number of characters and is multibyte character aware.
105
+ #
106
+ # Prior to this, Ruby's multibyte character support in Strings was a
107
+ # bit unusual and neither String#length nor String#split behave as
108
+ # expected, so we use String#scan to split all the consumed text using
109
+ # a UTF-8 aware regex and use the length of the result
110
+ def consumed_chars
111
+ return scanner.charpos if scanner.respond_to?(:charpos)
112
+
113
+ (scanner.pre_match + scanner.matched).scan(%r{.}mu).length
114
+ end
115
+
116
+ def start_interp
117
+ if @segment.last && @segment.last == '\\'
118
+ read_char
119
+ return
120
+ end
121
+
122
+ if interp_stack.empty?
123
+ scanner.skip(START_INTERP_PATTERN)
124
+ results << [@segment_type, @segment.join]
125
+ @segment = []
126
+ else
127
+ @segment << scanner.scan(START_INTERP_PATTERN)
128
+ end
129
+
130
+ interp_stack.push(true)
131
+ end
132
+
133
+ def end_interp
134
+ if interp_stack.empty?
135
+ @segment << scanner.scan(END_INTERP_PATTERN)
136
+ return
137
+ else
138
+ interp_stack.pop
139
+ end
140
+
141
+ if interp_stack.empty?
142
+ results << [:INTERP, @segment.join]
143
+ @segment = []
144
+ scanner.skip(END_INTERP_PATTERN)
145
+ else
146
+ @segment << scanner.scan(END_INTERP_PATTERN)
147
+ end
148
+ end
149
+
150
+ def unenclosed_variable
151
+ read_char if scanner.match?(%r{.\$})
152
+
153
+ results << [@segment_type, @segment.join]
154
+ results << [:UNENC_VAR, scanner.scan(UNENC_VAR_PATTERN)]
155
+ @segment = []
156
+ end
157
+
158
+ def end_heredoc(pattern)
159
+ results << [:HEREDOC, @segment.join]
160
+ results << [:HEREDOC_TERM, scanner.scan(pattern)]
161
+ end
162
+
163
+ def end_string
164
+ if interp_stack.empty?
165
+ @segment << scanner.scan(END_STRING_PATTERN).gsub!(%r{"\Z}, '')
166
+ results << [@segment_type, @segment.join]
167
+ else
168
+ @segment << scanner.scan(END_STRING_PATTERN)
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -193,10 +193,18 @@ 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
199
201
  end
202
+
203
+ def interpolated_variable?
204
+ return false if type == :TYPE && value != 'type'
205
+ return true if type == :NAME
206
+ PuppetLint::Lexer::KEYWORDS.include?(type.to_s.downcase)
207
+ end
200
208
  end
201
209
  end
202
210
  end
@@ -18,7 +18,6 @@ class PuppetLint::OptParser
18
18
  #
19
19
  # Returns an OptionParser object.
20
20
  def self.build(args = [])
21
- noconfig = false
22
21
  opt_parser = OptionParser.new do |opts|
23
22
  opts.banner = HELP_TEXT
24
23
 
@@ -27,7 +26,7 @@ class PuppetLint::OptParser
27
26
  end
28
27
 
29
28
  opts.on('--no-config', 'Do not load default puppet-lint option files.') do
30
- noconfig = true
29
+ # nothing to do, option is handled differently
31
30
  end
32
31
 
33
32
  opts.on('-c', '--config FILE', 'Load puppet-lint options from file.') do |file|
@@ -134,9 +133,7 @@ class PuppetLint::OptParser
134
133
  end
135
134
  end
136
135
 
137
- opt_parser.parse!(args) unless args.empty?
138
-
139
- unless noconfig
136
+ unless args.include?('--no-config')
140
137
  opt_parser.load('/etc/puppet-lint.rc')
141
138
  if ENV.key?('HOME') && File.readable?(ENV['HOME'])
142
139
  home_dotfile_path = File.expand_path('~/.puppet-lint.rc')
@@ -145,6 +142,8 @@ class PuppetLint::OptParser
145
142
  opt_parser.load('.puppet-lint.rc')
146
143
  end
147
144
 
145
+ opt_parser.parse!(args) unless args.empty?
146
+
148
147
  opt_parser
149
148
  end
150
149
  end
@@ -22,6 +22,7 @@ PuppetLint.new_check(:parameter_order) do
22
22
  end
23
23
 
24
24
  next unless hash_or_array_stack.empty? && paren_stack.empty?
25
+ next unless parameter?(token)
25
26
  next unless required_parameter?(token)
26
27
 
27
28
  prev_tokens = class_idx[:param_tokens][0..i]
@@ -38,9 +39,19 @@ PuppetLint.new_check(:parameter_order) do
38
39
  end
39
40
  end
40
41
 
41
- def required_parameter?(token)
42
+ def parameter?(token)
42
43
  return false unless token.type == :VARIABLE
44
+ return false unless token.prev_code_token
45
+
46
+ [
47
+ :LPAREN, # First parameter, no type specification
48
+ :COMMA, # Subsequent parameter, no type specification
49
+ :TYPE, # Parameter with simple type specification
50
+ :RBRACK, # Parameter with complex type specification
51
+ ].include?(token.prev_code_token.type)
52
+ end
43
53
 
54
+ def required_parameter?(token)
44
55
  data_type = token.prev_token_of(:TYPE, :skip_blocks => true)
45
56
  return false if data_type && data_type.value == 'Optional'
46
57
 
@@ -31,7 +31,21 @@ PuppetLint.new_check(:case_without_default) do
31
31
  case_tokens -= tokens[successor_kase[:start]..successor_kase[:end]]
32
32
  end
33
33
 
34
- next if case_tokens.index { |r| r.type == :DEFAULT }
34
+ found_matching_default = false
35
+ depth = 0
36
+
37
+ case_tokens.each do |token|
38
+ if token.type == :LBRACE
39
+ depth += 1
40
+ elsif token.type == :RBRACE
41
+ depth -= 1
42
+ elsif token.type == :DEFAULT && depth == 1
43
+ found_matching_default = true
44
+ break
45
+ end
46
+ end
47
+
48
+ next if found_matching_default
35
49
 
36
50
  notify(
37
51
  :warning,
@@ -30,8 +30,12 @@ PuppetLint.new_check(:documentation) do
30
30
  end
31
31
 
32
32
  def find_comment_token(start_token)
33
+ newlines = 0
34
+
33
35
  prev_token = start_token.prev_token
34
36
  while !prev_token.nil? && WHITESPACE_TOKENS.include?(prev_token.type)
37
+ newlines += 1 if prev_token.type == :NEWLINE
38
+ break if newlines > 1
35
39
  prev_token = prev_token.prev_token
36
40
  end
37
41
 
@@ -26,12 +26,15 @@ PuppetLint.new_check(:ensure_first_param) do
26
26
  end
27
27
 
28
28
  def fix(problem)
29
- first_param_name_token = tokens[problem[:resource][:start]].next_token_of(:NAME)
29
+ first_param_name_token = problem[:resource][:param_tokens].first
30
30
  first_param_comma_token = first_param_name_token.next_token_of(:COMMA)
31
31
  ensure_param_name_token = first_param_comma_token.next_token_of(:NAME, :value => 'ensure')
32
+
33
+ raise PuppetLint::NoFix if ensure_param_name_token.nil?
34
+
32
35
  ensure_param_comma_token = ensure_param_name_token.next_token_of([:COMMA, :SEMIC])
33
36
 
34
- if first_param_name_token.nil? || first_param_comma_token.nil? || ensure_param_name_token.nil? || ensure_param_comma_token.nil?
37
+ if first_param_name_token.nil? || first_param_comma_token.nil? || ensure_param_comma_token.nil?
35
38
  raise PuppetLint::NoFix
36
39
  end
37
40
 
@@ -25,3 +25,4 @@ PuppetLint.new_check(:quoted_booleans) do
25
25
  problem[:token].type = problem[:token].value.upcase.to_sym
26
26
  end
27
27
  end
28
+ PuppetLint.configuration.send('disable_quoted_booleans')
@@ -1,9 +1,19 @@
1
+ require 'set'
2
+ require 'strscan'
3
+
1
4
  # Public: Check the manifest tokens for any variables in a string that have
2
5
  # not been enclosed by braces ({}) and record a warning for each instance
3
6
  # found.
4
7
  #
5
8
  # https://puppet.com/docs/puppet/latest/style_guide.html#quoting
6
9
  PuppetLint.new_check(:variables_not_enclosed) do
10
+ STRING_TOKEN_TYPES = Set[
11
+ :DQMID,
12
+ :DQPOST,
13
+ :HEREDOC_MID,
14
+ :HEREDOC_POST,
15
+ ]
16
+
7
17
  def check
8
18
  tokens.select { |r|
9
19
  r.type == :UNENC_VARIABLE
@@ -18,7 +28,68 @@ PuppetLint.new_check(:variables_not_enclosed) do
18
28
  end
19
29
  end
20
30
 
31
+ def hash_or_array_ref?(token)
32
+ token.next_token &&
33
+ STRING_TOKEN_TYPES.include?(token.next_token.type) &&
34
+ token.next_token.value.start_with?('[')
35
+ end
36
+
37
+ def extract_hash_or_array_ref(token)
38
+ scanner = StringScanner.new(token.value)
39
+
40
+ brack_depth = 0
41
+ result = { :ref => '' }
42
+
43
+ until scanner.eos?
44
+ result[:ref] += scanner.getch
45
+
46
+ # Pass a length of 1 when slicing the last character from the string
47
+ # to prevent Ruby 1.8 returning a Fixnum instead of a String.
48
+ case result[:ref][-1, 1]
49
+ when '['
50
+ brack_depth += 1
51
+ when ']'
52
+ brack_depth -= 1
53
+ end
54
+
55
+ break if brack_depth.zero? && scanner.peek(1) != '['
56
+ end
57
+
58
+ result[:remainder] = scanner.rest
59
+ result
60
+ end
61
+
62
+ def variable_contains_dash?(token)
63
+ token.value.include?('-')
64
+ end
65
+
66
+ def handle_variable_containing_dash(var_token)
67
+ str_token = var_token.next_token
68
+
69
+ var_name, text = var_token.value.split('-', 2)
70
+ var_token.value = var_name
71
+
72
+ return if str_token.nil?
73
+ str_token.value = "-#{text}#{str_token.value}"
74
+ end
75
+
21
76
  def fix(problem)
22
77
  problem[:token].type = :VARIABLE
78
+
79
+ if hash_or_array_ref?(problem[:token])
80
+ string_token = problem[:token].next_token
81
+ tokens_index = tokens.index(string_token)
82
+
83
+ hash_or_array_ref = extract_hash_or_array_ref(string_token)
84
+
85
+ ref_tokens = PuppetLint::Lexer.new.tokenise(hash_or_array_ref[:ref])
86
+ ref_tokens.each_with_index do |token, i|
87
+ add_token(tokens_index + i, token)
88
+ end
89
+
90
+ string_token.value = hash_or_array_ref[:remainder]
91
+ end
92
+
93
+ handle_variable_containing_dash(problem[:token]) if variable_contains_dash?(problem[:token])
23
94
  end
24
95
  end
@@ -51,7 +51,7 @@ PuppetLint.new_check(:arrow_alignment) do
51
51
  this_arrow_column = param_column[level_idx] + param_length + 1
52
52
  else
53
53
  this_arrow_column = param_token.column + param_token.to_manifest.length
54
- this_arrow_column += 1 unless param_token.type == :DQPOST
54
+ this_arrow_column += 1
55
55
  end
56
56
 
57
57
  if arrow_column[level_idx] < this_arrow_column
@@ -20,6 +20,7 @@ class PuppetLint
20
20
  attr_accessor :ignore_paths
21
21
  attr_accessor :with_filename
22
22
  attr_accessor :disable_checks
23
+ attr_accessor :only_checks
23
24
  attr_accessor :fail_on_warnings
24
25
  attr_accessor :error_level
25
26
  attr_accessor :log_format
@@ -40,6 +41,7 @@ class PuppetLint
40
41
  @pattern = DEFAULT_PATTERN
41
42
  @with_filename = true
42
43
  @disable_checks = []
44
+ @only_checks = []
43
45
  @ignore_paths = []
44
46
 
45
47
  define(args, &task_block)
@@ -55,6 +57,17 @@ class PuppetLint
55
57
  task @name do
56
58
  PuppetLint::OptParser.build
57
59
 
60
+ if Array(@only_checks).any?
61
+ enable_checks = Array(@only_checks).map(&:to_sym)
62
+ PuppetLint.configuration.checks.each do |check|
63
+ if enable_checks.include?(check)
64
+ PuppetLint.configuration.send("enable_#{check}")
65
+ else
66
+ PuppetLint.configuration.send("disable_#{check}")
67
+ end
68
+ end
69
+ end
70
+
58
71
  Array(@disable_checks).each do |check|
59
72
  PuppetLint.configuration.send("disable_#{check}")
60
73
  end
@@ -79,6 +92,7 @@ class PuppetLint
79
92
  matched_files = matched_files.exclude(*@ignore_paths)
80
93
 
81
94
  matched_files.to_a.each do |puppet_file|
95
+ next unless File.file?(puppet_file)
82
96
  linter.file = puppet_file
83
97
  linter.run
84
98
  linter.print_problems