foodcritic 1.7.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,6 @@
1
1
  require 'pathname'
2
2
  require 'gherkin'
3
3
  require 'treetop'
4
- require 'pry'
5
4
  require 'rak'
6
5
  require 'ripper'
7
6
  require 'yajl'
@@ -15,7 +14,6 @@ require_relative 'foodcritic/notifications'
15
14
  require_relative 'foodcritic/ast'
16
15
  require_relative 'foodcritic/xml'
17
16
  require_relative 'foodcritic/api'
18
- require_relative 'foodcritic/repl'
19
17
  require_relative 'foodcritic/dsl'
20
18
  require_relative 'foodcritic/linter'
21
19
  require_relative 'foodcritic/output'
@@ -34,16 +34,18 @@ module FoodCritic
34
34
  # Does the specified recipe check for Chef Solo?
35
35
  def checks_for_chef_solo?(ast)
36
36
  raise_unless_xpath!(ast)
37
-
38
37
  # TODO: This expression is too loose, but also will fail to match other
39
38
  # types of conditionals.
40
- ! ast.xpath(%q{//*[self::if or self::unless]/*[self::aref or self::call]
39
+ (! ast.xpath(%q{//*[self::if or self::unless]/*[self::aref or
40
+ child::aref or self::call]
41
41
  [count(descendant::const[@value = 'Chef' or @value = 'Config']) = 2
42
42
  and
43
43
  ( count(descendant::ident[@value='solo']) > 0
44
44
  or count(descendant::tstring_content[@value='solo']) > 0
45
45
  )
46
- ]}).empty?
46
+ ]}).empty?) ||
47
+ ast.xpath('//if_mod[return][aref/descendant::ident/@value="solo"]/aref/
48
+ const_path_ref/descendant::const').map{|c|c['value']} == %w{Chef Config}
47
49
  end
48
50
 
49
51
  # Is the [chef-solo-search library](https://github.com/edelight/chef-solo-search)
@@ -105,12 +107,7 @@ module FoodCritic
105
107
  # %w{foo bar baz}.each do |cbk|
106
108
  # depends cbk
107
109
  # end
108
- var_ref = ast.xpath(%q{//command[ident/@value='depends']/
109
- descendant::var_ref/ident})
110
- unless var_ref.empty?
111
- deps += ast.xpath(%Q{//block_var/params/ident#{var_ref.first['value']}/
112
- ancestor::method_add_block/call/descendant::tstring_content})
113
- end
110
+ deps = deps.to_a + word_list_values(ast, "//command[ident/@value='depends']")
114
111
  deps.map{|dep| dep['value']}
115
112
  end
116
113
 
@@ -191,16 +188,6 @@ module FoodCritic
191
188
  :line => pos['line'].to_i, :column => pos['column'].to_i}
192
189
  end
193
190
 
194
- # Does the provided string look like an Operating System command? This is a
195
- # rough heuristic to be taken with a pinch of salt.
196
- def os_command?(str)
197
- str.start_with?('grep ', 'net ', 'which ') or # common commands
198
- str.include?('|') or # a pipe, could be alternation
199
- str.include?('/') or # file path delimiter
200
- str.match(/^[\w]+$/) or # command name only
201
- str.match(/ --?[a-z]/i) # command-line flag
202
- end
203
-
204
191
  # Read the AST for the given Ruby source file
205
192
  def read_ast(file)
206
193
  source = if file.to_s.end_with? '.erb'
@@ -298,6 +285,20 @@ module FoodCritic
298
285
  templates}
299
286
  end
300
287
 
288
+ # Platforms declared as supported in cookbook metadata
289
+ def supported_platforms(ast)
290
+ platforms = ast.xpath('//command[ident/@value="supports"]/
291
+ descendant::*[self::string_literal or self::symbol_literal][position() = 1]
292
+ [self::symbol_literal or count(descendant::string_add) = 1]/
293
+ descendant::*[self::tstring_content | self::ident]')
294
+ platforms = platforms.to_a + word_list_values(ast, "//command[ident/@value='supports']")
295
+ platforms.map do |platform|
296
+ versions = platform.xpath('ancestor::args_add[position() > 1]/
297
+ string_literal/descendant::tstring_content/@value').map{|v| v.to_s}
298
+ {:platform => platform['value'], :versions => versions}
299
+ end.sort{|a,b| a[:platform] <=> b[:platform]}
300
+ end
301
+
301
302
  # Template filename
302
303
  def template_file(resource)
303
304
  if resource['source']
@@ -441,13 +442,20 @@ module FoodCritic
441
442
  standard_attribute_access(ast, options.merge(:type => type))
442
443
  end.inject(:+)
443
444
  else
444
- type = options[:type] == :string ? 'tstring_content' : options[:type]
445
- expr = '//*[self::aref_field or self::aref]'
446
- expr += '[count(descendant::aref/var_ref) = 0]'
445
+ type = if options[:type] == :string
446
+ 'tstring_content'
447
+ else
448
+ '*[self::symbol or self::dyna_symbol]'
449
+ end
450
+ expr = '//*[self::aref_field or self::aref][count(method_add_arg) = 0]'
451
+ expr += '[count(is_att_type(descendant::var_ref/ident/@value)) =
452
+ count(descendant::var_ref/ident/@value)]'
447
453
  expr += '[is_att_type(descendant::ident'
448
454
  expr += '[not(ancestor::aref/call)]' if options[:ignore_calls]
449
455
  expr += "/@value)]/descendant::#{type}"
450
- expr += "[ident/@value != 'node']" if type == :symbol
456
+ if options[:type] == :string
457
+ expr += '[count(ancestor::dyna_symbol) = 0]'
458
+ end
451
459
  ast.xpath(expr, AttFilter.new).sort
452
460
  end
453
461
  end
@@ -463,6 +471,16 @@ module FoodCritic
463
471
  end.sort
464
472
  end
465
473
 
474
+ def word_list_values(ast, xpath)
475
+ var_ref = ast.xpath("#{xpath}/descendant::var_ref/ident")
476
+ if var_ref.empty?
477
+ []
478
+ else
479
+ ast.xpath(%Q{descendant::block_var/params/ident#{var_ref.first['value']}/
480
+ ancestor::method_add_block/call/descendant::tstring_content})
481
+ end
482
+ end
483
+
466
484
  end
467
485
 
468
486
  end
@@ -12,16 +12,12 @@ module FoodCritic
12
12
  @options = {:fail_tags => [], :tags => [], :include_rules => []}
13
13
  @parser = OptionParser.new do |opts|
14
14
  opts.banner = 'foodcritic [cookbook_paths]'
15
- opts.on("-r", "--[no-]repl",
16
- "Drop into a REPL for interactive rule editing.") do |r|
17
- options[:repl] = r
18
- end
19
15
  opts.on("-t", "--tags TAGS",
20
16
  "Only check against rules with the specified tags.") do |t|
21
17
  options[:tags] << t
22
18
  end
23
19
  opts.on("-f", "--epic-fail TAGS",
24
- "Fail the build if any of the specified tags are matched.") do |t|
20
+ "Fail the build if any of the specified tags are matched ('any' -> fail on any match).") do |t|
25
21
  options[:fail_tags] << t
26
22
  end
27
23
  opts.on("-c", "--chef-version VERSION",
@@ -86,14 +82,6 @@ module FoodCritic
86
82
  "foodcritic #{FoodCritic::VERSION}"
87
83
  end
88
84
 
89
- # If the cookbook path provided is valid
90
- #
91
- # @deprecated Multiple cookbook paths may be provided.
92
- # @return [Boolean] True if the path exists.
93
- def valid_path?
94
- @args.length == 1 and File.exists?(@args[0])
95
- end
96
-
97
85
  # If the cookbook paths provided are valid
98
86
  #
99
87
  # @return [Boolean] True if the paths exist.
@@ -101,14 +89,6 @@ module FoodCritic
101
89
  @args.any? && @args.all? {|path| File.exists?(path) }
102
90
  end
103
91
 
104
- # The cookbook path.
105
- #
106
- # @deprecated Multiple cookbook paths may be provided.
107
- # @return [String] Path to the cookbook(s) being checked.
108
- def cookbook_path
109
- @args[0]
110
- end
111
-
112
92
  # The cookbook paths
113
93
  #
114
94
  # @return [Array<String>] Path(s) to the cookbook(s) being checked.
@@ -25,12 +25,6 @@ module FoodCritic
25
25
  @is_failed = is_failed
26
26
  end
27
27
 
28
- # Provided for backwards compatibility. Deprecated and will be removed in a
29
- # later version.
30
- def cookbook_path
31
- @cookbook_paths.first
32
- end
33
-
34
28
  # If this review has failed or not.
35
29
  def failed?
36
30
  @is_failed
@@ -65,17 +65,13 @@ module FoodCritic
65
65
  rule_block :template
66
66
 
67
67
  # Load the ruleset(s).
68
- def self.load(paths, with_repl)
68
+ def self.load(paths)
69
69
  dsl = RuleDsl.new
70
70
  paths.map do |path|
71
71
  File.directory?(path) ? Dir["#{path}/**/*.rb"].sort : path
72
72
  end.flatten.each do |path|
73
73
  dsl.instance_eval(File.read(path), path)
74
74
  end
75
-
76
- # Drop into the REPL for exploratory rule development.
77
- dsl.instance_eval { binding.pry } if with_repl
78
-
79
75
  dsl.rules
80
76
  end
81
77
 
@@ -9,7 +9,6 @@ module FoodCritic
9
9
  class Linter
10
10
 
11
11
  include FoodCritic::Api
12
- include FoodCritic::REPL
13
12
 
14
13
  # The default version that will be used to determine relevant rules. This
15
14
  # can be over-ridden at the command line with the `--chef-version` option.
@@ -47,57 +46,73 @@ module FoodCritic
47
46
 
48
47
  cookbook_paths = sanity_check_cookbook_paths(cookbook_paths)
49
48
  options = setup_defaults(options)
49
+ @options = options
50
50
 
51
- # Enable checks to be easily repeated at the REPL
52
- with_repl(cookbook_paths, options) do
53
- warnings = []; last_dir = nil; matched_rule_tags = Set.new
51
+ warnings = []; last_dir = nil; matched_rule_tags = Set.new
54
52
 
55
- load_rules
53
+ load_rules
56
54
 
57
- # Loop through each file to be processed and apply the rules
58
- files_to_process(cookbook_paths, options[:exclude_paths]).each do |file|
59
- ast = read_ast(file)
60
- active_rules(options).each do |rule|
61
- rule_matches = matches(rule.recipe, ast, file)
55
+ # Loop through each file to be processed and apply the rules
56
+ files_to_process(cookbook_paths, options[:exclude_paths]).each do |file|
57
+ ast = read_ast(file)
58
+ active_rules(options).each do |rule|
59
+ rule_matches = matches(rule.recipe, ast, file)
62
60
 
63
- if dsl_method_for_file(file)
64
- rule_matches += matches(rule.send(dsl_method_for_file(file)),
65
- ast, file)
66
- end
61
+ if dsl_method_for_file(file)
62
+ rule_matches += matches(rule.send(dsl_method_for_file(file)),
63
+ ast, file)
64
+ end
67
65
 
68
- per_cookbook_rules(last_dir, file) do
69
- if File.basename(file) == 'metadata.rb'
70
- rule_matches += matches(rule.metadata, ast, file)
71
- end
72
- rule_matches += matches(rule.cookbook, cookbook_dir(file))
66
+ per_cookbook_rules(last_dir, file) do
67
+ if File.basename(file) == 'metadata.rb'
68
+ rule_matches += matches(rule.metadata, ast, file)
73
69
  end
70
+ rule_matches += matches(rule.cookbook, cookbook_dir(file))
71
+ end
74
72
 
75
- # Convert the matches into warnings
76
- rule_matches.each do |match|
77
- warnings << Warning.new(rule, {:filename => file}.merge(match))
78
- matched_rule_tags << rule.tags
79
- end
73
+ rule_matches = remove_ignored(rule_matches, rule, file)
74
+
75
+ # Convert the matches into warnings
76
+ rule_matches.each do |match|
77
+ warnings << Warning.new(rule, {:filename => file}.merge(match))
78
+ matched_rule_tags << rule.tags
80
79
  end
81
- last_dir = cookbook_dir(file)
82
80
  end
83
-
84
- Review.new(cookbook_paths, warnings,
85
- should_fail_build?(options[:fail_tags], matched_rule_tags))
81
+ last_dir = cookbook_dir(file)
86
82
  end
83
+
84
+ Review.new(cookbook_paths, warnings,
85
+ should_fail_build?(options[:fail_tags], matched_rule_tags))
87
86
  end
88
87
 
89
88
  # Load the rules from the (fairly unnecessary) DSL.
90
89
  def load_rules
91
- load_rules!(@last_options) unless defined? @rules
90
+ load_rules!(@options) unless defined? @rules
92
91
  end
93
92
 
94
93
  def load_rules!(options)
95
94
  @rules = RuleDsl.load([File.join(File.dirname(__FILE__), 'rules.rb')] +
96
- options[:include_rules], options[:repl])
95
+ options[:include_rules])
97
96
  end
98
97
 
99
98
  private
100
99
 
100
+ def remove_ignored(matches, rule, file)
101
+ matches.reject do |m|
102
+ (line = m[:line]) && File.exist?(file) &&
103
+ ignore_line_match?(File.readlines(file)[line-1], rule)
104
+ end
105
+ end
106
+
107
+ def ignore_line_match?(line, rule)
108
+ ignores = line.to_s[/\s+#\s*(.*)/, 1]
109
+ if ignores and ignores.include?('~')
110
+ ! matching_tags?(ignores.split(/[ ,]/), rule.tags)
111
+ else
112
+ false
113
+ end
114
+ end
115
+
101
116
  # Some rules are version specific.
102
117
  def applies_to_version?(rule, version)
103
118
  return true unless version
@@ -207,7 +207,8 @@ rule "FC017", "LWRP does not notify when updated" do
207
207
  version >= gem_version("0.7.12")
208
208
  end
209
209
  provider do |ast, filename|
210
- if ast.xpath(%q{//call/*[self::vcall or self::var_ref/ident/
210
+ if ast.xpath(%q{//*[self::call or self::command_call]/
211
+ *[self::vcall or self::var_ref/ident/
211
212
  @value='new_resource']/../
212
213
  ident[@value='updated_by_last_action']}).empty?
213
214
  [file_match(filename)]
@@ -229,23 +230,26 @@ end
229
230
  rule "FC019", "Access node attributes in a consistent manner" do
230
231
  tags %w{style attributes}
231
232
  cookbook do |cookbook_dir|
232
- asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].map do |file|
233
+ asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].reject do |file|
234
+ relative_path = Pathname.new(file).relative_path_from(Pathname.new(cookbook_dir))
235
+ relative_path.to_s.split(File::SEPARATOR).include?('spec')
236
+ end.map do |file|
233
237
  {:path => file, :ast => read_ast(file)}
234
238
  end
235
239
  types = [:string, :symbol, :vivified].map do |type|
236
240
  {:access_type => type, :count => files.map do |file|
237
241
  attribute_access(file[:ast], :type => type, :ignore_calls => true,
238
242
  :cookbook_dir => cookbook_dir).tap do |ast|
239
- if (! ast.empty?) and (! asts.has_key?(type))
240
- asts[type] = {:ast => ast, :path => file[:path]}
243
+ unless ast.empty?
244
+ (asts[type] ||= []) << {:ast => ast, :path => file[:path]}
241
245
  end
242
246
  end.size
243
247
  end.inject(:+)}
244
248
  end.reject{|type| type[:count] == 0}
245
249
  if asts.size > 1
246
250
  least_used = asts[types.min{|a,b| a[:count] <=> b[:count]}[:access_type]]
247
- least_used[:ast].map do |ast|
248
- match(ast).merge(:filename => least_used[:path])
251
+ least_used.map do |file|
252
+ file[:ast].map{|ast| match(ast).merge(:filename => file[:path])}.flatten
249
253
  end
250
254
  end
251
255
  end
@@ -307,17 +311,26 @@ end
307
311
  rule "FC024", "Consider adding platform equivalents" do
308
312
  tags %w{portability}
309
313
  RHEL = %w{amazon centos redhat scientific}
310
- recipe do |ast|
314
+ recipe do |ast, filename|
315
+ next if Pathname.new(filename).basename.to_s == 'metadata.rb'
316
+ metadata_path = Pathname.new(
317
+ File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
318
+ md_platforms = if File.exists?(metadata_path)
319
+ supported_platforms(read_ast(metadata_path)).map{|p| p[:platform]}
320
+ else
321
+ []
322
+ end
323
+ md_platforms = RHEL if md_platforms.empty?
324
+
311
325
  ['//method_add_arg[fcall/ident/@value="platform?"]/arg_paren/args_add_block',
312
326
  "//when"].map do |expr|
313
327
  ast.xpath(expr).map do |whn|
314
328
  platforms = whn.xpath("args_add/descendant::tstring_content").map do |p|
315
329
  p['value']
316
- end
317
- unless platforms.size == 1 || (RHEL & platforms).empty?
318
- unless (RHEL - platforms).empty?
319
- whn
320
- end
330
+ end.sort
331
+ unless platforms.size == 1 || (md_platforms & platforms).empty?
332
+ whn unless (platforms & RHEL).empty? ||
333
+ ((md_platforms & RHEL) - (platforms & RHEL)).empty?
321
334
  end
322
335
  end.compact
323
336
  end.flatten
@@ -476,7 +489,7 @@ rule "FC037", "Invalid notification action" do
476
489
  when :notifies then n[:resource_type]
477
490
  when :subscribes then resource_type(resource).to_sym
478
491
  end
479
- ! resource_action?(type, n[:action])
492
+ n[:action].size > 0 and ! resource_action?(type, n[:action])
480
493
  end
481
494
  end
482
495
  end
@@ -522,9 +535,14 @@ end
522
535
  rule "FC040", "Execute resource used to run git commands" do
523
536
  tags %w{style recipe etsy}
524
537
  recipe do |ast|
538
+ possible_git_commands = %w{ clone fetch pull checkout reset }
525
539
  find_resources(ast, :type => 'execute').select do |cmd|
526
540
  cmd_str = (resource_attribute(cmd, 'command') || resource_name(cmd)).to_s
527
- cmd_str.include?('git ')
541
+
542
+ git_cmd = cmd_str.match(/git ([a-z]+)/)
543
+ break false if git_cmd.nil?
544
+
545
+ !git_cmd.captures.nil? && possible_git_commands.include?(git_cmd.captures[0])
528
546
  end
529
547
  end
530
548
  end
@@ -548,6 +566,7 @@ end
548
566
 
549
567
  rule "FC043", "Prefer new notification syntax" do
550
568
  tags %w{style notifications deprecated}
569
+ applies_to {|version| version >= gem_version("0.9.10")}
551
570
  recipe do |ast|
552
571
  find_resources(ast).select do |resource|
553
572
  notifications(resource).any?{|notify| notify[:style] == :old}
@@ -561,7 +580,10 @@ rule "FC044", "Avoid bare attribute keys" do
561
580
  declared = ast.xpath('//descendant::var_field/ident/@value').map{|v| v.to_s}
562
581
  ast.xpath('//assign/*[self::vcall or self::var_ref]
563
582
  [count(child::kw) = 0]/ident').select do |v|
564
- (v['value'] != 'secure_password') && ! declared.include?(v['value'])
583
+ (v['value'] != 'secure_password') &&
584
+ ! declared.include?(v['value']) &&
585
+ ! v.xpath("ancestor::*[self::brace_block or self::do_block]/block_var/
586
+ descendant::ident/@value='#{v['value']}'")
565
587
  end
566
588
  end
567
589
  end
@@ -579,3 +601,9 @@ rule "FC045", "Consider setting cookbook name in metadata" do
579
601
  end
580
602
  end
581
603
  end
604
+
605
+ rule "FC046", "Attribute assignment uses assign unless nil" do
606
+ attributes do |ast|
607
+ attribute_access(ast).map{|a| a.xpath('ancestor::opassign/op[@value="||="]')}
608
+ end
609
+ end
@@ -1,4 +1,4 @@
1
1
  module FoodCritic
2
2
  # The current version of foodcritic
3
- VERSION = '1.7.0'
3
+ VERSION = '2.0.0'
4
4
  end