foodcritic 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG.md +83 -0
  3. data/chef_dsl_metadata/chef_0.10.0.json +2 -3
  4. data/chef_dsl_metadata/chef_0.10.10.json +2 -3
  5. data/chef_dsl_metadata/chef_0.10.2.json +2 -3
  6. data/chef_dsl_metadata/chef_0.10.4.json +2 -3
  7. data/chef_dsl_metadata/chef_0.10.6.json +2 -3
  8. data/chef_dsl_metadata/chef_0.10.8.json +2 -3
  9. data/chef_dsl_metadata/chef_0.8.14.json +2 -3
  10. data/chef_dsl_metadata/chef_0.8.16.json +2 -3
  11. data/chef_dsl_metadata/chef_0.9.0.json +2 -3
  12. data/chef_dsl_metadata/chef_0.9.10.json +2 -3
  13. data/chef_dsl_metadata/chef_0.9.12.json +2 -3
  14. data/chef_dsl_metadata/chef_0.9.14.json +2 -3
  15. data/chef_dsl_metadata/chef_0.9.16.json +2 -3
  16. data/chef_dsl_metadata/chef_0.9.18.json +2 -3
  17. data/chef_dsl_metadata/chef_0.9.2.json +2 -3
  18. data/chef_dsl_metadata/chef_0.9.4.json +2 -3
  19. data/chef_dsl_metadata/chef_0.9.6.json +2 -3
  20. data/chef_dsl_metadata/chef_0.9.8.json +2 -3
  21. data/chef_dsl_metadata/chef_10.12.0.json +2 -3
  22. data/chef_dsl_metadata/chef_10.14.0.json +2 -3
  23. data/chef_dsl_metadata/chef_10.14.2.json +2 -3
  24. data/chef_dsl_metadata/chef_10.14.4.json +2 -3
  25. data/chef_dsl_metadata/chef_10.16.0.json +2 -3
  26. data/chef_dsl_metadata/chef_10.16.2.json +2 -3
  27. data/chef_dsl_metadata/chef_10.16.4.json +2 -3
  28. data/chef_dsl_metadata/chef_10.16.6.json +2 -3
  29. data/chef_dsl_metadata/chef_10.18.0.json +2 -3
  30. data/chef_dsl_metadata/chef_10.18.2.json +2 -3
  31. data/chef_dsl_metadata/chef_10.20.0.json +2 -3
  32. data/chef_dsl_metadata/chef_10.22.0.json +2 -3
  33. data/chef_dsl_metadata/chef_10.24.0.json +2 -3
  34. data/chef_dsl_metadata/chef_10.24.4.json +2 -3
  35. data/chef_dsl_metadata/chef_10.26.0.json +2 -3
  36. data/chef_dsl_metadata/chef_11.0.0.json +2 -3
  37. data/chef_dsl_metadata/chef_11.2.0.json +2 -3
  38. data/chef_dsl_metadata/chef_11.4.0.json +2 -3
  39. data/chef_dsl_metadata/chef_11.4.2.json +2 -3
  40. data/chef_dsl_metadata/chef_11.4.4.json +2 -3
  41. data/chef_dsl_metadata/chef_11.6.0.json +9734 -0
  42. data/features/007_check_for_undeclared_recipe_dependencies.feature +18 -34
  43. data/features/017_check_for_no_lwrp_notifications.feature +25 -0
  44. data/features/019_check_for_consistent_node_access.feature +1 -0
  45. data/features/033_check_for_missing_template.feature +20 -64
  46. data/features/034_check_for_unused_template_variables.feature +44 -0
  47. data/features/047_check_for_attribute_assignment_without_precedence.feature +47 -0
  48. data/features/048_check_for_shellout.feature +34 -0
  49. data/features/049_check_for_role_name_mismatch_with_file_name.feature +31 -0
  50. data/features/050_check_for_invalid_name.feature +33 -0
  51. data/features/051_check_for_template_partial_loops.feature +21 -0
  52. data/features/command_line_help.feature +15 -0
  53. data/features/ignore_via_line_comments.feature +18 -0
  54. data/features/individual_file.feature +17 -1
  55. data/features/multiple_paths.feature +26 -2
  56. data/features/step_definitions/cookbook_steps.rb +328 -9
  57. data/features/support/command_helpers.rb +71 -10
  58. data/features/support/cookbook_helpers.rb +88 -6
  59. data/lib/foodcritic/api.rb +89 -20
  60. data/lib/foodcritic/command_line.rb +64 -18
  61. data/lib/foodcritic/domain.rb +26 -7
  62. data/lib/foodcritic/dsl.rb +3 -0
  63. data/lib/foodcritic/linter.rb +93 -61
  64. data/lib/foodcritic/rake_task.rb +3 -2
  65. data/lib/foodcritic/rules.rb +105 -14
  66. data/lib/foodcritic/template.rb +34 -1
  67. data/lib/foodcritic/version.rb +1 -1
  68. data/man/foodcritic.1 +13 -1
  69. data/man/foodcritic.1.ronn +9 -0
  70. data/spec/foodcritic/api_spec.rb +210 -1
  71. data/spec/foodcritic/command_line_spec.rb +13 -0
  72. data/spec/foodcritic/domain_spec.rb +40 -5
  73. data/spec/foodcritic/linter_spec.rb +19 -22
  74. data/spec/foodcritic/template_spec.rb +8 -4
  75. data/spec/regression/expected-output.txt +139 -60
  76. metadata +31 -26
@@ -9,40 +9,59 @@ module FoodCritic
9
9
  def initialize(args)
10
10
  @args = args
11
11
  @original_args = args.dup
12
- @options = {:fail_tags => [], :tags => [], :include_rules => []}
12
+ @options = {
13
+ :fail_tags => [],
14
+ :tags => [],
15
+ :include_rules => [],
16
+ :cookbook_paths => [],
17
+ :role_paths => [],
18
+ :environment_paths => []
19
+ }
13
20
  @parser = OptionParser.new do |opts|
14
21
  opts.banner = 'foodcritic [cookbook_paths]'
15
22
  opts.on("-t", "--tags TAGS",
16
23
  "Only check against rules with the specified tags.") do |t|
17
- options[:tags] << t
24
+ @options[:tags] << t
18
25
  end
19
26
  opts.on("-f", "--epic-fail TAGS",
20
27
  "Fail the build if any of the specified tags are matched ('any' -> fail on any match).") do |t|
21
- options[:fail_tags] << t
28
+ @options[:fail_tags] << t
22
29
  end
23
30
  opts.on("-c", "--chef-version VERSION",
24
31
  "Only check against rules valid for this version of Chef.") do |c|
25
- options[:chef_version] = c
32
+ @options[:chef_version] = c
33
+ end
34
+ opts.on("-B", "--cookbook-path PATH",
35
+ "Cookbook path(s) to check.") do |b|
36
+ @options[:cookbook_paths] << b
26
37
  end
27
38
  opts.on("-C", "--[no-]context",
28
39
  "Show lines matched against rather than the default summary.") do |c|
29
- options[:context] = c
40
+ @options[:context] = c
41
+ end
42
+ opts.on("-E", "--environment-path PATH",
43
+ "Environment path(s) to check.") do |e|
44
+ @options[:environment_paths] << e
30
45
  end
31
46
  opts.on("-I", "--include PATH",
32
47
  "Additional rule file path(s) to load.") do |i|
33
- options[:include_rules] << i
48
+ @options[:include_rules] << i
34
49
  end
35
50
  opts.on("-G", "--search-gems",
36
51
  "Search rubygems for rule files with the path foodcritic/rules/**/*.rb") do |g|
37
- options[:search_gems] = true
52
+ @options[:search_gems] = true
53
+ end
54
+ opts.on("-R", "--role-path PATH",
55
+ "Role path(s) to check.") do |r|
56
+ @options[:role_paths] << r
38
57
  end
39
58
  opts.on("-S", "--search-grammar PATH",
40
59
  "Specify grammar to use when validating search syntax.") do |s|
41
- options[:search_grammar] = s
60
+ @options[:search_grammar] = s
42
61
  end
43
62
  opts.on("-V", "--version",
44
63
  "Display the foodcritic version.") do |v|
45
- options[:version] = true
64
+ @options[:version] = true
46
65
  end
47
66
  end
48
67
  # -v is not implemented but OptionParser gives the Foodcritic's version
@@ -86,18 +105,13 @@ module FoodCritic
86
105
  "foodcritic #{FoodCritic::VERSION}"
87
106
  end
88
107
 
89
- # If the cookbook paths provided are valid
108
+ # If the paths provided are valid
90
109
  #
91
110
  # @return [Boolean] True if the paths exist.
92
111
  def valid_paths?
93
- @args.any? && @args.all? {|path| File.exists?(path) }
94
- end
95
-
96
- # The cookbook paths
97
- #
98
- # @return [Array<String>] Path(s) to the cookbook(s) being checked.
99
- def cookbook_paths
100
- @args
112
+ paths = options[:cookbook_paths] + options[:role_paths] +
113
+ options[:environment_paths]
114
+ paths.any? && paths.all?{|path| File.exists?(path) }
101
115
  end
102
116
 
103
117
  # Is the search grammar specified valid?
@@ -112,6 +126,27 @@ module FoodCritic
112
126
  search.parser?
113
127
  end
114
128
 
129
+ # The cookbook paths to check
130
+ #
131
+ # @return [Array<String>] Path(s) to the cookbook(s) being checked.
132
+ def cookbook_paths
133
+ @args + Array(@options[:cookbook_paths])
134
+ end
135
+
136
+ # The role paths to check
137
+ #
138
+ # @return [Array<String>] Path(s) to the role directories being checked.
139
+ def role_paths
140
+ Array(@options[:role_paths])
141
+ end
142
+
143
+ # The environment paths to check
144
+ #
145
+ # @return [Array<String>] Path(s) to the environment directories being checked.
146
+ def environment_paths
147
+ Array(@options[:environment_paths])
148
+ end
149
+
115
150
  # If matches should be shown with context rather than the default summary
116
151
  # display.
117
152
  #
@@ -124,6 +159,17 @@ module FoodCritic
124
159
  #
125
160
  # @return [Hash] The parsed command-line options.
126
161
  def options
162
+ original_options.merge({
163
+ :cookbook_paths => cookbook_paths,
164
+ :role_paths => role_paths,
165
+ :environment_paths => environment_paths,
166
+ })
167
+ end
168
+
169
+ # The original command-line options
170
+ #
171
+ # @return [Hash] The original command-line options.
172
+ def original_options
127
173
  @options
128
174
  end
129
175
 
@@ -1,16 +1,24 @@
1
+ require 'gherkin/tag_expression'
2
+
1
3
  module FoodCritic
2
4
 
3
5
  # A warning of a possible issue
4
6
  class Warning
5
- attr_reader :rule, :match
7
+ attr_reader :rule, :match, :is_failed
6
8
 
7
9
  # Create a new warning.
8
10
  #
9
11
  # Warning.new(rule, :filename => 'foo/recipes.default.rb',
10
12
  # :line => 5, :column=> 40)
11
13
  #
12
- def initialize(rule, match={})
14
+ def initialize(rule, match={}, options={})
13
15
  @rule, @match = rule, match
16
+ @is_failed = options[:fail_tags].empty? ? false : rule.matches_tags?(options[:fail_tags])
17
+ end
18
+
19
+ # If this warning has failed or not.
20
+ def failed?
21
+ @is_failed
14
22
  end
15
23
  end
16
24
 
@@ -19,15 +27,19 @@ module FoodCritic
19
27
 
20
28
  attr_reader :cookbook_paths, :warnings
21
29
 
22
- def initialize(cookbook_paths, warnings, is_failed)
30
+ def initialize(cookbook_paths, warnings)
23
31
  @cookbook_paths = Array(cookbook_paths)
24
32
  @warnings = warnings
25
- @is_failed = is_failed
26
33
  end
27
34
 
28
- # If this review has failed or not.
35
+ # If any of the warnings in this review have failed or not.
29
36
  def failed?
30
- @is_failed
37
+ warnings.any? { |w| w.failed? }
38
+ end
39
+
40
+ # Returns an array of warnings that are marked as failed.
41
+ def failures
42
+ warnings.select { |w| w.failed? }
31
43
  end
32
44
 
33
45
  # Returns a string representation of this review. This representation is
@@ -48,7 +60,7 @@ module FoodCritic
48
60
  # A rule to be matched against.
49
61
  class Rule
50
62
  attr_accessor :code, :name, :applies_to, :cookbook, :attributes, :recipe,
51
- :provider, :resource, :metadata, :library, :template
63
+ :provider, :resource, :metadata, :library, :template, :role, :environment
52
64
 
53
65
  attr_writer :tags
54
66
 
@@ -64,6 +76,13 @@ module FoodCritic
64
76
  ['any'] + @tags
65
77
  end
66
78
 
79
+ # Checks the rule's tags to see if they match a Gherkin (Cucumber) expression
80
+ def matches_tags?(tag_expr)
81
+ Gherkin::TagExpression.new(tag_expr).evaluate(tags.map do |t|
82
+ Gherkin::Formatter::Model::Tag.new(t, 1)
83
+ end)
84
+ end
85
+
67
86
  # Returns a string representation of this rule.
68
87
  def to_s
69
88
  "#{@code}: #{@name}"
@@ -69,6 +69,9 @@ module FoodCritic
69
69
  rule_block :library
70
70
  rule_block :template
71
71
 
72
+ rule_block :environment
73
+ rule_block :role
74
+
72
75
  # Load the ruleset(s).
73
76
  def self.load(paths, chef_version=Linter::DEFAULT_CHEF_VERSION)
74
77
  dsl = RuleDsl.new(chef_version)
@@ -1,7 +1,6 @@
1
1
  require 'optparse'
2
2
  require 'ripper'
3
3
  require 'rubygems'
4
- require 'gherkin/tag_expression'
5
4
  require 'set'
6
5
 
7
6
  module FoodCritic
@@ -25,8 +24,7 @@ module FoodCritic
25
24
  if ! cmd_line.valid_grammar?
26
25
  [cmd_line.help, 4]
27
26
  elsif cmd_line.valid_paths?
28
- review = FoodCritic::Linter.new.check(cmd_line.cookbook_paths,
29
- cmd_line.options)
27
+ review = FoodCritic::Linter.new.check(cmd_line.options)
30
28
  [review, review.failed? ? 3 : 0]
31
29
  else
32
30
  [cmd_line.help, 2]
@@ -38,55 +36,86 @@ module FoodCritic
38
36
  #
39
37
  # The `options` are a hash where the valid keys are:
40
38
  #
39
+ # * `:cookbook_paths` - Cookbook paths to lint
40
+ # * `:role_paths` - Role paths to lint
41
41
  # * `:include_rules` - Paths to additional rules to apply
42
42
  # * `:search_gems - If true then search for custom rules in installed gems.
43
43
  # * `:tags` - The tags to filter rules based on
44
44
  # * `:fail_tags` - The tags to fail the build on
45
45
  # * `:exclude_paths` - Paths to exclude from linting
46
46
  #
47
- def check(cookbook_paths, options = {})
47
+ def check(options = {})
48
48
 
49
- cookbook_paths = sanity_check_cookbook_paths(cookbook_paths)
50
49
  options = setup_defaults(options)
51
50
  @options = options
52
51
  @chef_version = options[:chef_version] || DEFAULT_CHEF_VERSION
53
52
 
54
53
  warnings = []; last_dir = nil; matched_rule_tags = Set.new
55
-
56
54
  load_rules
55
+ paths = specified_paths!(options)
57
56
 
58
57
  # Loop through each file to be processed and apply the rules
59
- files_to_process(cookbook_paths, options[:exclude_paths]).each do |file|
60
- ast = read_ast(file)
61
- relevant_tags = options[:tags].any? ? options[:tags] : cookbook_tags(file)
62
- active_rules(relevant_tags).each do |rule|
63
- rule_matches = matches(rule.recipe, ast, file)
58
+ files_to_process(paths).each do |p|
64
59
 
65
- if dsl_method_for_file(file)
66
- rule_matches += matches(rule.send(dsl_method_for_file(file)),
67
- ast, file)
68
- end
60
+ relevant_tags = if options[:tags].any?
61
+ options[:tags]
62
+ else
63
+ cookbook_tags(p[:filename])
64
+ end
69
65
 
70
- per_cookbook_rules(last_dir, file) do
71
- if File.basename(file) == 'metadata.rb'
72
- rule_matches += matches(rule.metadata, ast, file)
73
- end
74
- rule_matches += matches(rule.cookbook, cookbook_dir(file))
66
+ active_rules(relevant_tags).each do |rule|
67
+
68
+ state = {
69
+ :path_type => p[:path_type],
70
+ :file => p[:filename],
71
+ :ast => read_ast(p[:filename]),
72
+ :rule => rule,
73
+ :last_dir => last_dir
74
+ }
75
+
76
+ matches = if p[:path_type] == :cookbook
77
+ cookbook_matches(state)
78
+ else
79
+ other_matches(state)
75
80
  end
76
81
 
77
- rule_matches = remove_ignored(rule_matches, rule, file)
82
+ matches = remove_ignored(matches, state[:rule], state[:file])
78
83
 
79
84
  # Convert the matches into warnings
80
- rule_matches.each do |match|
81
- warnings << Warning.new(rule, {:filename => file}.merge(match))
82
- matched_rule_tags << rule.tags
85
+ matches.each do |match|
86
+ warnings << Warning.new(state[:rule],
87
+ {:filename => state[:file]}.merge(match), options)
88
+ matched_rule_tags << state[:rule].tags
83
89
  end
84
90
  end
85
- last_dir = cookbook_dir(file)
91
+ last_dir = cookbook_dir(p[:filename])
92
+ end
93
+
94
+ Review.new(paths, warnings)
95
+ end
96
+
97
+ def cookbook_matches(state)
98
+ cbk_matches = matches(state[:rule].recipe, state[:ast], state[:file])
99
+
100
+ if dsl_method_for_file(state[:file])
101
+ cbk_matches += matches(state[:rule].send(
102
+ dsl_method_for_file(state[:file])), state[:ast], state[:file])
103
+ end
104
+
105
+ per_cookbook_rules(state[:last_dir], state[:file]) do
106
+ if File.basename(state[:file]) == 'metadata.rb'
107
+ cbk_matches += matches(
108
+ state[:rule].metadata, state[:ast], state[:file])
109
+ end
110
+ cbk_matches += matches(
111
+ state[:rule].cookbook, cookbook_dir(state[:file]))
86
112
  end
87
113
 
88
- Review.new(cookbook_paths, warnings,
89
- should_fail_build?(options[:fail_tags], matched_rule_tags))
114
+ cbk_matches
115
+ end
116
+
117
+ def other_matches(state)
118
+ matches(state[:rule].send(state[:path_type]), state[:ast], state[:file])
90
119
  end
91
120
 
92
121
  # Load the rules from the (fairly unnecessary) DSL.
@@ -120,7 +149,7 @@ module FoodCritic
120
149
  def ignore_line_match?(line, rule)
121
150
  ignores = line.to_s[/\s+#\s*(.*)/, 1]
122
151
  if ignores and ignores.include?('~')
123
- ! matching_tags?(ignores.split(/[ ,]/), rule.tags)
152
+ ! rule.matches_tags?(ignores.split(/[ ,]/))
124
153
  else
125
154
  false
126
155
  end
@@ -147,7 +176,7 @@ module FoodCritic
147
176
 
148
177
  def active_rules(tags)
149
178
  @rules.select do |rule|
150
- matching_tags?(tags, rule.tags) and
179
+ rule.matches_tags?(tags) and
151
180
  applies_to_version?(rule, chef_version)
152
181
  end
153
182
  end
@@ -178,19 +207,29 @@ module FoodCritic
178
207
 
179
208
  # Return the files within a cookbook tree that we are interested in trying
180
209
  # to match rules against.
181
- def files_to_process(dirs, exclude_paths = [])
182
- files = []
183
- dirs.each do |dir|
184
- exclusions = Dir.glob(exclude_paths.map{|p| File.join(dir, p)})
185
- if File.directory? dir
186
- cookbook_glob = '{metadata.rb,{attributes,definitions,libraries,providers,recipes,resources}/*.rb,templates/*/*.erb}'
187
- files += (Dir.glob(File.join(dir, cookbook_glob)) +
188
- Dir.glob(File.join(dir, "*/#{cookbook_glob}")) - exclusions)
189
- else
190
- files << dir unless exclusions.include?(dir)
210
+ def files_to_process(paths)
211
+ paths.reject{|type, _| type == :exclude}.map do |path_type, dirs|
212
+ dirs.map do |dir|
213
+ exclusions = []
214
+ unless paths[:exclude].empty?
215
+ exclusions = Dir.glob(paths[:exclude].map{|p| File.join(dir, p)})
216
+ end
217
+
218
+ if File.directory?(dir)
219
+ glob = if path_type == :cookbook
220
+ '{metadata.rb,{attributes,definitions,libraries,providers,recipes,resources}/*.rb,templates/*/*.erb}'
221
+ else
222
+ '*.rb'
223
+ end
224
+ (Dir.glob(File.join(dir, glob)) +
225
+ Dir.glob(File.join(dir, "*/#{glob}")) - exclusions)
226
+ else
227
+ dir unless exclusions.include?(dir)
228
+ end
229
+ end.compact.flatten.map do |filename|
230
+ {:filename => filename, :path_type => path_type}
191
231
  end
192
- end
193
- files
232
+ end.flatten
194
233
  end
195
234
 
196
235
  # Invoke the DSL method with the provided parameters.
@@ -211,34 +250,27 @@ module FoodCritic
211
250
  end.flatten
212
251
  end
213
252
 
214
- # We use the Gherkin (Cucumber) syntax to specify tags.
215
- def matching_tags?(tag_expr, tags)
216
- Gherkin::TagExpression.new(tag_expr).evaluate(tags.map do |t|
217
- Gherkin::Formatter::Model::Tag.new(t, 1)
218
- end)
219
- end
220
-
221
253
  def per_cookbook_rules(last_dir, file)
222
254
  yield if last_dir != cookbook_dir(file)
223
255
  end
224
256
 
225
- def sanity_check_cookbook_paths(cookbook_paths)
226
- raise ArgumentError, "Cookbook paths are required" if cookbook_paths.nil?
227
- cookbook_paths = Array(cookbook_paths)
228
- if cookbook_paths.empty?
229
- raise ArgumentError, "Cookbook paths cannot be empty"
257
+ def specified_paths!(options)
258
+ paths = Hash[options.map do |key, value|
259
+ [key, Array(value)] if key.to_s.end_with?('paths')
260
+ end.compact]
261
+
262
+ unless paths.find{|k, v| k != :exclude_paths and ! v.empty?}
263
+ raise ArgumentError, "A cookbook path or role path must be specified"
230
264
  end
231
- cookbook_paths
232
- end
233
265
 
234
- def setup_defaults(options)
235
- {:tags => [], :fail_tags => [],
236
- :include_rules => [], :exclude_paths => []}.merge(options)
266
+ Hash[paths.map do |key, value|
267
+ [key.to_s.sub(/_paths$/, '').to_sym, value]
268
+ end]
237
269
  end
238
270
 
239
- def should_fail_build?(fail_tags, matched_tags)
240
- return false if fail_tags.empty?
241
- matched_tags.any?{|tags| matching_tags?(fail_tags, tags)}
271
+ def setup_defaults(options)
272
+ {:tags => [], :fail_tags => [], :include_rules => [], :exclude_paths => [],
273
+ :cookbook_paths => [], :role_paths => []}.merge(options)
242
274
  end
243
275
 
244
276
  end