foodcritic 1.0.1 → 1.1.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.
@@ -22,24 +22,11 @@ module FoodCritic
22
22
  raise ArgumentError, "Node type not recognised"
23
23
  end
24
24
 
25
- (if options[:type] == :vivified
26
- calls = ast.xpath(%q{//*[self::call or self::field]
27
- [is_att_type(vcall/ident/@value) or
28
- is_att_type(var_ref/ident/@value)][@value='.']}, AttFilter.new)
29
- calls.select do |call|
30
- call.xpath("aref/args_add_block").size == 0 and
31
- (call.xpath("descendant::ident").size > 1 and
32
- ! chef_dsl_methods.include?(call.xpath("ident/@value").to_s.to_sym))
33
- end
25
+ if options[:type] == :vivified
26
+ vivified_attribute_access(ast)
34
27
  else
35
- type = (options[:type] == :string) ? 'tstring_content' : options[:type]
36
- expr = '//*[self::aref_field or self::aref]'
37
- expr += '[is_att_type(descendant::ident'
38
- expr += '[not(ancestor::aref/call)]' if options[:ignore_calls]
39
- expr += "/@value)]/descendant::#{type}"
40
- ast.xpath(expr, AttFilter.new)
28
+ standard_attribute_access(ast, options)
41
29
  end
42
- ).sort
43
30
  end
44
31
 
45
32
  # Does the specified recipe check for Chef Solo?
@@ -61,7 +48,8 @@ module FoodCritic
61
48
  def chef_solo_search_supported?(recipe_path)
62
49
  return false if recipe_path.nil? || ! File.exists?(recipe_path)
63
50
  cbk_tree_path = Pathname.new(File.join(recipe_path, '../../..'))
64
- search_libs = Dir[File.join(cbk_tree_path.realpath, "*/libraries/search.rb")]
51
+ search_libs = Dir[File.join(cbk_tree_path.realpath,
52
+ '*/libraries/search.rb')]
65
53
  search_libs.any? do |lib|
66
54
  ! read_ast(lib).xpath(%q{//class[count(descendant::const[@value='Chef']
67
55
  ) = 1]/descendant::def/ident[@value='search']}).empty?
@@ -78,6 +66,12 @@ module FoodCritic
78
66
  file = File.absolute_path(File.dirname(file.to_s))
79
67
  end
80
68
  file = File.dirname(file) unless File.extname(file).empty?
69
+ md_path = File.join(file, 'metadata.rb')
70
+ if File.exists?(md_path)
71
+ name = read_ast(md_path).xpath("//stmts_add/
72
+ command[ident/@value='name']/descendant::tstring_content/@value").to_s
73
+ return name unless name.empty?
74
+ end
81
75
  File.basename(file)
82
76
  end
83
77
 
@@ -88,7 +82,7 @@ module FoodCritic
88
82
  def declared_dependencies(ast)
89
83
  raise_unless_xpath!(ast)
90
84
  deps = ast.xpath(%q{//command[ident/@value='depends']/
91
- descendant::args_add/descendant::tstring_content})
85
+ descendant::args_add/descendant::tstring_content[1]})
92
86
  # handle quoted word arrays
93
87
  var_ref = ast.xpath(%q{//command[ident/@value='depends']/
94
88
  descendant::var_ref/ident})
@@ -138,6 +132,7 @@ module FoodCritic
138
132
  included.inject(Hash.new([])){|h, i| h[i['value']] += [i]; h}
139
133
  end
140
134
 
135
+ # XPath custom function
141
136
  class AttFilter
142
137
  def is_att_type(value)
143
138
  return [] unless value.respond_to?(:select)
@@ -159,7 +154,8 @@ module FoodCritic
159
154
  # Create a match from the specified node.
160
155
  #
161
156
  # @param [Nokogiri::XML::Node] node The node to create a match for
162
- # @return [Hash] Hash with the matched node name and position with the recipe
157
+ # @return [Hash] Hash with the matched node name and position with the
158
+ # recipe
163
159
  def match(node)
164
160
  raise_unless_xpath!(node)
165
161
  pos = node.xpath('descendant::pos').first
@@ -175,7 +171,7 @@ module FoodCritic
175
171
  # @return [Boolean] True if this string might be an OS command
176
172
  def os_command?(str)
177
173
  str.start_with?('grep ', 'which ') or # common commands
178
- str.include?('|') or # probably a pipe, could be alternation
174
+ str.include?('|') or # a pipe, could be alternation
179
175
  str.match(/^[\w]+$/) or # command name only
180
176
  str.match(/ --?[a-z]/i) # command-line flag
181
177
  end
@@ -212,7 +208,8 @@ module FoodCritic
212
208
  if att.xpath('descendant::symbol').empty?
213
209
  att_value = att.xpath('string(descendant::tstring_content/@value)')
214
210
  else
215
- att_value = att.xpath('string(descendant::symbol/ident/@value)').to_sym
211
+ att_value =
212
+ att.xpath('string(descendant::symbol/ident/@value)').to_sym
216
213
  end
217
214
  atts[att.xpath('string(ident/@value)')] = att_value
218
215
  end
@@ -346,6 +343,26 @@ module FoodCritic
346
343
  end
347
344
  end
348
345
 
346
+ def standard_attribute_access(ast, options)
347
+ type = options[:type] == :string ? 'tstring_content' : options[:type]
348
+ expr = '//*[self::aref_field or self::aref]'
349
+ expr += '[is_att_type(descendant::ident'
350
+ expr += '[not(ancestor::aref/call)]' if options[:ignore_calls]
351
+ expr += "/@value)]/descendant::#{type}"
352
+ ast.xpath(expr, AttFilter.new).sort
353
+ end
354
+
355
+ def vivified_attribute_access(ast)
356
+ calls = ast.xpath(%q{//*[self::call or self::field]
357
+ [is_att_type(vcall/ident/@value) or
358
+ is_att_type(var_ref/ident/@value)][@value='.']}, AttFilter.new)
359
+ calls.select do |call|
360
+ call.xpath("aref/args_add_block").size == 0 and
361
+ (call.xpath("descendant::ident").size > 1 and
362
+ ! chef_dsl_methods.include?(call.xpath("ident/@value").to_s.to_sym))
363
+ end.sort
364
+ end
365
+
349
366
  end
350
367
 
351
368
  end
@@ -8,7 +8,7 @@ module FoodCritic
8
8
  # @return [Array] Array of method symbols
9
9
  def chef_dsl_methods
10
10
  load_metadata
11
- @dsl_metadata[:dsl_methods].map{|m| m.to_sym}
11
+ @dsl_metadata[:dsl_methods].map(&:to_sym)
12
12
  end
13
13
 
14
14
  # Is the specified attribute valid for the type of resource? Note that this
@@ -16,6 +16,7 @@ module FoodCritic
16
16
  #
17
17
  # @param [Symbol] resource_type The type of Chef resource
18
18
  # @param [Symbol] attribute_name The attribute name
19
+ # @return [Boolean] False if the attribute is known not to be valid
19
20
  def resource_attribute?(resource_type, attribute_name)
20
21
  if resource_type.to_s.empty? || attribute_name.to_s.empty?
21
22
  raise ArgumentError, "Arguments cannot be nil or empty."
@@ -46,6 +47,7 @@ module FoodCritic
46
47
  :symbolize_keys => true)
47
48
  end
48
49
 
50
+ # Chef Search
49
51
  class Search
50
52
 
51
53
  # The search grammars that ship with any Chef gems installed locally.
@@ -67,7 +69,8 @@ module FoodCritic
67
69
  begin
68
70
  break parser unless parser.nil?
69
71
  # don't instantiate custom nodes
70
- Treetop.load_from_string(IO.read(lucene_grammar).gsub(/<[^>]+>/, ''))
72
+ Treetop.load_from_string(
73
+ IO.read(lucene_grammar).gsub(/<[^>]+>/, ''))
71
74
  LuceneParser.new
72
75
  rescue
73
76
  # silently swallow and try the next grammar
@@ -82,6 +85,9 @@ module FoodCritic
82
85
  ! @search_parser.nil?
83
86
  end
84
87
 
88
+ # The search parser
89
+ #
90
+ # @return [LuceneParser] The search parser
85
91
  def parser
86
92
  @search_parser
87
93
  end
@@ -9,17 +9,37 @@ module FoodCritic
9
9
  def initialize(args)
10
10
  @args = args
11
11
  @original_args = args.dup
12
- @options = {}
13
- @options[:fail_tags] = []; @options[:tags] = []; @options[:include_rules] = []
12
+ @options = {:fail_tags => [], :tags => [], :include_rules => []}
14
13
  @parser = OptionParser.new do |opts|
15
14
  opts.banner = 'foodcritic [cookbook_path]'
16
- opts.on("-r", "--[no-]repl", "Drop into a REPL for interactive rule editing.") {|r|options[:repl] = r}
17
- opts.on("-t", "--tags TAGS", "Only check against rules with the specified tags.") {|t|options[:tags] << t}
18
- opts.on("-f", "--epic-fail TAGS", "Fail the build if any of the specified tags are matched.") {|t|options[:fail_tags] << t}
19
- opts.on("-C", "--[no-]context", "Show lines matched against rather than the default summary.") {|c|options[:context] = c}
20
- opts.on("-I", "--include PATH", "Additional rule file path(s) to load.") {|i|options[:include_rules] << i}
21
- opts.on("-S", "--search-grammar PATH", "Specify grammar to use when validating search syntax.") {|s|options[:search_grammar] = s}
22
- opts.on("-V", "--version", "Display version."){|v|options[:version] = true}
15
+ opts.on("-r", "--[no-]repl",
16
+ "Drop into a REPL for interactive rule editing.") do |r|
17
+ options[:repl] = r
18
+ end
19
+ opts.on("-t", "--tags TAGS",
20
+ "Only check against rules with the specified tags.") do |t|
21
+ options[:tags] << t
22
+ end
23
+ opts.on("-f", "--epic-fail TAGS",
24
+ "Fail the build if any of the specified tags are matched.") do |t|
25
+ options[:fail_tags] << t
26
+ end
27
+ opts.on("-C", "--[no-]context",
28
+ "Show lines matched against rather than the default summary.") do |c|
29
+ options[:context] = c
30
+ end
31
+ opts.on("-I", "--include PATH",
32
+ "Additional rule file path(s) to load.") do |i|
33
+ options[:include_rules] << i
34
+ end
35
+ opts.on("-S", "--search-grammar PATH",
36
+ "Specify grammar to use when validating search syntax.") do |s|
37
+ options[:search_grammar] = s
38
+ end
39
+ opts.on("-V", "--version",
40
+ "Display version.") do |v|
41
+ options[:version] = true
42
+ end
23
43
  end
24
44
  @parser.parse!(args) unless show_help?
25
45
  end
@@ -68,7 +88,8 @@ module FoodCritic
68
88
 
69
89
  # Is the search grammar specified valid?
70
90
  #
71
- # @return [Boolean] True if the grammar has not been provided or can be loaded.
91
+ # @return [Boolean] True if the grammar has not been provided or can be
92
+ # loaded.
72
93
  def valid_grammar?
73
94
  return true unless options.key?(:search_grammar)
74
95
  return false unless File.exists?(options[:search_grammar])
@@ -77,7 +98,8 @@ module FoodCritic
77
98
  search.parser?
78
99
  end
79
100
 
80
- # If matches should be shown with context rather than the default summary display.
101
+ # If matches should be shown with context rather than the default summary
102
+ # display.
81
103
  #
82
104
  # @return [Boolean] True if matches should be shown with context.
83
105
  def show_context?
@@ -8,7 +8,8 @@ module FoodCritic
8
8
  #
9
9
  # @param [FoodCritic::Rule] rule The rule which raised this warning
10
10
  # @param [Hash] match The match data
11
- # @option match [String] :filename The filename the warning was raised against
11
+ # @option match [String] :filename The filename the warning was raised
12
+ # against
12
13
  # @option match [Integer] :line The identified line
13
14
  # @option match [Integer] :column The identified column
14
15
  def initialize(rule, match={})
@@ -25,7 +26,8 @@ module FoodCritic
25
26
  #
26
27
  # @param [String] cookbook_path The path this review was performed against
27
28
  # @param [Array] warnings The warnings raised in this review
28
- # @param [Boolean] is_failed Have warnings been raised that mean this should be considered failed?
29
+ # @param [Boolean] is_failed Have warnings been raised that mean this
30
+ # should be considered failed?
29
31
  def initialize(cookbook_path, warnings, is_failed)
30
32
  @cookbook_path = cookbook_path
31
33
  @warnings = warnings
@@ -41,9 +43,13 @@ module FoodCritic
41
43
 
42
44
  # Returns a string representation of this review.
43
45
  #
44
- # @return [String] Review as a string, this representation is liable to change.
46
+ # @return [String] Review as a string, this representation is liable to
47
+ # change.
45
48
  def to_s
46
- @warnings.map{|w|["#{w.rule.code}: #{w.rule.name}: #{w.match[:filename]}", w.match[:line].to_i]}.sort do |x,y|
49
+ @warnings.map do |w|
50
+ ["#{w.rule.code}: #{w.rule.name}: #{w.match[:filename]}",
51
+ w.match[:line].to_i]
52
+ end.sort do |x,y|
47
53
  x.first == y.first ? x[1] <=> y[1] : x.first <=> y.first
48
54
  end.map{|w|"#{w.first}:#{w[1]}"}.uniq.join("\n")
49
55
  end
@@ -56,13 +62,19 @@ module FoodCritic
56
62
 
57
63
  # Create a new rule
58
64
  #
59
- # @param [String] code The short unique identifier for this rule, e.g. 'FC001'
60
- # @param [String] name The short descriptive name of this rule presented to the end user.
65
+ # @param [String] code The short unique identifier for this rule,
66
+ # e.g. 'FC001'
67
+ # @param [String] name The short descriptive name of this rule presented to
68
+ # the end user.
61
69
  def initialize(code, name)
62
70
  @code, @name = code, name
63
71
  @tags = [code]
64
72
  end
65
73
 
74
+ # The tags associated with this rule.
75
+ # A rule is always tagged with the tags 'any' and the rule code.
76
+ #
77
+ # @return [Array] The tags associated.
66
78
  def tags
67
79
  ['any'] + @tags
68
80
  end
@@ -11,7 +11,8 @@ module FoodCritic
11
11
 
12
12
  # Define a new rule
13
13
  #
14
- # @param [String] code The short unique identifier for this rule, e.g. 'FC001'
14
+ # @param [String] code The short unique identifier for this rule,
15
+ # e.g. 'FC001'
15
16
  # @param [String] name The short descriptive name of this rule presented to
16
17
  # the end user.
17
18
  # @param [Block] block The rule definition
@@ -18,7 +18,8 @@ module FoodCritic
18
18
 
19
19
  # Register with all available error handlers.
20
20
  def self.register_error_handlers
21
- SexpBuilder.public_instance_methods.grep(/^on_.*_error$/).sort.each do |err_meth|
21
+ error_methods = SexpBuilder.public_instance_methods.grep(/^on_.*_error$/)
22
+ error_methods.sort.each do |err_meth|
22
23
  define_method(err_meth) { |*| @found_error = true }
23
24
  end
24
25
  end
@@ -54,13 +54,20 @@ module FoodCritic
54
54
  active_rules = @rules.select{|rule| matching_tags?(options[:tags],
55
55
  rule.tags)}
56
56
  files_to_process(cookbook_path).each do |file|
57
- cookbook_dir = Pathname.new(File.join(File.dirname(file), '..')).cleanpath
57
+ cookbook_dir = Pathname.new(
58
+ File.join(File.dirname(file), '..')).cleanpath
58
59
  ast = read_ast(file)
59
60
  active_rules.each do |rule|
60
61
  rule_matches = matches(rule.recipe, ast, file)
61
- rule_matches += matches(rule.provider, ast, file) if File.basename(File.dirname(file)) == 'providers'
62
- rule_matches += matches(rule.resource, ast, file) if File.basename(File.dirname(file)) == 'resources'
63
- rule_matches += matches(rule.cookbook, cookbook_dir) if last_dir != cookbook_dir
62
+ if File.basename(File.dirname(file)) == 'providers'
63
+ rule_matches += matches(rule.provider, ast, file)
64
+ end
65
+ if File.basename(File.dirname(file)) == 'resources'
66
+ rule_matches += matches(rule.resource, ast, file)
67
+ end
68
+ if last_dir != cookbook_dir
69
+ rule_matches += matches(rule.cookbook, cookbook_dir)
70
+ end
64
71
  rule_matches.each do |match|
65
72
  warnings << Warning.new(rule, {:filename => file}.merge(match))
66
73
  matched_rule_tags << rule.tags
@@ -76,7 +83,8 @@ module FoodCritic
76
83
  @review
77
84
  end
78
85
 
79
- # Convenience method to repeat the last check. Intended to be used from the REPL.
86
+ # Convenience method to repeat the last check. Intended to be used from the
87
+ # REPL.
80
88
  def recheck
81
89
  check(@last_cookbook_path, @last_options)
82
90
  end
@@ -107,7 +115,16 @@ module FoodCritic
107
115
  def matches(match_method, *params)
108
116
  return [] unless match_method.respond_to?(:yield)
109
117
  matches = match_method.yield(*params)
110
- matches.respond_to?(:each) ? matches : []
118
+ return [] unless matches.respond_to?(:each)
119
+ matches.map do |m|
120
+ if m.respond_to?(:node_name)
121
+ match(m)
122
+ elsif m.respond_to?(:xpath)
123
+ m.to_a.map{|m| match(m)}
124
+ else
125
+ m
126
+ end
127
+ end.flatten
111
128
  end
112
129
 
113
130
  # Return the files within a cookbook tree that we are interested in trying
@@ -118,8 +135,9 @@ module FoodCritic
118
135
  # processed.
119
136
  def files_to_process(dir)
120
137
  return [dir] unless File.directory? dir
121
- Dir.glob(File.join(dir, '{attributes,providers,recipes,resources}/*.rb')) +
122
- Dir.glob(File.join(dir, '*/{attributes,providers,recipes,resources}/*.rb'))
138
+ cookbook_glob = '{attributes,providers,recipes,resources}/*.rb'
139
+ Dir.glob(File.join(dir, cookbook_glob)) +
140
+ Dir.glob(File.join(dir, "*/#{cookbook_glob}"))
123
141
  end
124
142
 
125
143
  # Whether to fail the build.
@@ -2,7 +2,8 @@ module FoodCritic
2
2
 
3
3
  # Default output showing a summary view.
4
4
  class SummaryOutput
5
- # Output a summary view only listing the matching rules, file and line number.
5
+ # Output a summary view only listing the matching rules, file and line
6
+ # number.
6
7
  #
7
8
  # @param [Review] review The review to output.
8
9
  def output(review)
@@ -21,10 +22,12 @@ module FoodCritic
21
22
  puts review; return
22
23
  end
23
24
 
24
- # Cheating here and mis-using Rak (Ruby port of Ack) to generate pretty colourised context.
25
+ # Cheating here and mis-using Rak (Ruby port of Ack) to generate pretty
26
+ # colourised context.
25
27
  #
26
- # Rak supports evaluating a custom expression as an alternative to a regex. Our expression consults a hash of the
27
- # matches found and then we let Rak take care of the presentation.
28
+ # Rak supports evaluating a custom expression as an alternative to a
29
+ # regex. Our expression consults a hash of the matches found and then we
30
+ # let Rak take care of the presentation.
28
31
  line_lookup = key_by_file_and_line(review)
29
32
  Rak.class_eval do
30
33
  const_set(:RULE_COLOUR, "\033[1;36m")
@@ -33,8 +36,9 @@ module FoodCritic
33
36
  ARGV.replace(['--context', '--eval', %q{
34
37
  # This code will be evaluated inline by Rak.
35
38
  fn = fn.split("\n").first
36
- if @warnings.key?(fn) and @warnings[fn].key?($.) # filename and line number
37
- rule_name = "#{RULE_COLOUR if opt[:colour]}#{@warnings[fn][$.].to_a.join("\n")}#{CLEAR_COLOURS}"
39
+ if @warnings.key?(fn) and @warnings[fn].key?($.) # filename and line no
40
+ rule_name = opt[:colour] ? RULE_COLOUR : ''
41
+ rule_name += "#{@warnings[fn][$.].to_a.join("\n")}#{CLEAR_COLOURS}"
38
42
  if ! displayed_filename
39
43
  fn = "#{fn}\n#{rule_name}"
40
44
  else
@@ -50,16 +54,20 @@ module FoodCritic
50
54
 
51
55
  private
52
56
 
53
- # Build a hash lookup by filename and line number for warnings found in the specified review.
57
+ # Build a hash lookup by filename and line number for warnings found in the
58
+ # specified review.
54
59
  #
55
60
  # @param [Review] review The review to convert.
56
61
  # @return [Hash] Nested hashes keyed by filename and line number.
57
62
  def key_by_file_and_line(review)
58
63
  warn_hash = {}
59
64
  review.warnings.each do |warning|
60
- filename = Pathname.new(warning.match[:filename]).cleanpath.to_s; line_num = warning.match[:line].to_i
65
+ filename = Pathname.new(warning.match[:filename]).cleanpath.to_s
66
+ line_num = warning.match[:line].to_i
61
67
  warn_hash[filename] = {} unless warn_hash.key?(filename)
62
- warn_hash[filename][line_num] = Set.new unless warn_hash[filename].key?(line_num)
68
+ unless warn_hash[filename].key?(line_num)
69
+ warn_hash[filename][line_num] = Set.new
70
+ end
63
71
  warn_hash[filename][line_num] << warning.rule
64
72
  end
65
73
  warn_hash
@@ -67,4 +75,4 @@ module FoodCritic
67
75
 
68
76
  end
69
77
 
70
- end
78
+ end
@@ -1,7 +1,8 @@
1
- rule "FC001", "Use strings in preference to symbols to access node attributes" do
1
+ rule "FC001",
2
+ "Use strings in preference to symbols to access node attributes" do
2
3
  tags %w{style attributes}
3
4
  recipe do |ast|
4
- attribute_access(ast, :type => :symbol).map{|ar| match(ar)}
5
+ attribute_access(ast, :type => :symbol)
5
6
  end
6
7
  end
7
8
 
@@ -9,14 +10,19 @@ rule "FC002", "Avoid string interpolation where not required" do
9
10
  tags %w{style strings}
10
11
  recipe do |ast|
11
12
  ast.xpath(%q{//string_literal[count(descendant::string_embexpr) = 1 and
12
- count(string_add/tstring_content|string_add/string_add/tstring_content) = 0]}).map{|str| match(str)}
13
+ count(string_add/tstring_content|string_add/string_add/tstring_content)
14
+ = 0]})
13
15
  end
14
16
  end
15
17
 
16
- rule "FC003", "Check whether you are running with chef server before using server-specific features" do
18
+ rule "FC003",
19
+ "Check whether you are running with chef server before using" +
20
+ " server-specific features" do
17
21
  tags %w{portability solo}
18
22
  recipe do |ast,filename|
19
- searches(ast).map{|s| match(s)} unless checks_for_chef_solo?(ast) or chef_solo_search_supported?(filename)
23
+ unless checks_for_chef_solo?(ast) or chef_solo_search_supported?(filename)
24
+ searches(ast)
25
+ end
20
26
  end
21
27
  end
22
28
 
@@ -25,38 +31,52 @@ rule "FC004", "Use a service resource to start and stop services" do
25
31
  recipe do |ast|
26
32
  find_resources(ast, :type => 'execute').find_all do |cmd|
27
33
  cmd_str = (resource_attribute(cmd, 'command') || resource_name(cmd)).to_s
28
- cmd_str.include?('/etc/init.d') || cmd_str.start_with?('service ') || cmd_str.start_with?('/sbin/service ') ||
29
- cmd_str.start_with?('start ') || cmd_str.start_with?('stop ') || cmd_str.start_with?('invoke-rc.d ')
30
- end.map{|cmd| match(cmd)}
34
+ cmd_str.include?('/etc/init.d') || cmd_str.start_with?('service ') ||
35
+ cmd_str.start_with?('/sbin/service ') ||
36
+ cmd_str.start_with?('start ') || cmd_str.start_with?('stop ') ||
37
+ cmd_str.start_with?('invoke-rc.d ')
38
+ end
31
39
  end
32
40
  end
33
41
 
34
42
  rule "FC005", "Avoid repetition of resource declarations" do
35
43
  tags %w{style}
36
44
  recipe do |ast|
37
- resources = find_resources(ast).map{|res| resource_attributes(res).merge({:type => resource_type(res),
38
- :ast => res})}.chunk{|res| res[:type]}.reject{|res| res[1].size < 3}
45
+ resources = find_resources(ast).map do |res|
46
+ resource_attributes(res).merge({:type => resource_type(res),
47
+ :ast => res})
48
+ end.chunk{|res| res[:type]}.reject{|res| res[1].size < 3}
39
49
  resources.map do |cont_res|
40
50
  first_resource = cont_res[1][0][:ast]
41
- # we have contiguous resources of the same type, but do they share the same attributes?
42
- sorted_atts = cont_res[1].map{|atts| atts.delete_if{|k| k == :ast}.to_a.sort{|x,y| x.first.to_s <=> y.first.to_s}}
43
- match(first_resource) if sorted_atts.all?{|att| (att - sorted_atts.inject{|atts,a| atts & a}).length == 1}
51
+ # we have contiguous resources of the same type, but do they share the
52
+ # same attributes?
53
+ sorted_atts = cont_res[1].map do |atts|
54
+ atts.delete_if{|k| k == :ast}.to_a.sort do |x,y|
55
+ x.first.to_s <=> y.first.to_s
56
+ end
57
+ end
58
+ first_resource if sorted_atts.all? do |att|
59
+ (att - sorted_atts.inject{|atts,a| atts & a}).length == 1
60
+ end
44
61
  end.compact
45
62
  end
46
63
  end
47
64
 
48
- rule "FC006", "Mode should be quoted or fully specified when setting file permissions" do
65
+ rule "FC006",
66
+ "Mode should be quoted or fully specified when setting file permissions" do
49
67
  tags %w{correctness files}
50
68
  recipe do |ast|
51
- ast.xpath(%q{//ident[@value='mode']/parent::command/descendant::int[string-length(@value) < 5 and not(starts-with(@value, "0") and string-length(@value) = 4)]/
52
- ancestor::method_add_block}).map{|resource| match(resource)}
69
+ ast.xpath(%q{//ident[@value='mode']/parent::command/
70
+ descendant::int[string-length(@value) < 5 and not(starts-with(@value, "0")
71
+ and string-length(@value) = 4)]/ancestor::method_add_block})
53
72
  end
54
73
  end
55
74
 
56
75
  rule "FC007", "Ensure recipe dependencies are reflected in cookbook metadata" do
57
76
  tags %w{correctness metadata}
58
77
  recipe do |ast,filename|
59
- metadata_path = Pathname.new(File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
78
+ metadata_path =Pathname.new(
79
+ File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
60
80
  next unless File.exists? metadata_path
61
81
  undeclared = included_recipes(ast).keys.map do |recipe|
62
82
  recipe.split('::').first
@@ -65,7 +85,7 @@ rule "FC007", "Ensure recipe dependencies are reflected in cookbook metadata" do
65
85
  included_recipes(ast).map do |recipe, include_stmts|
66
86
  if undeclared.include?(recipe) ||
67
87
  undeclared.any?{|u| recipe.start_with?("#{u}::")}
68
- include_stmts.map{|i| match(i)}
88
+ include_stmts
69
89
  end
70
90
  end.flatten.compact
71
91
  end
@@ -77,8 +97,10 @@ rule "FC008", "Generated cookbook metadata needs updating" do
77
97
  metadata_path = Pathname.new(File.join(filename, 'metadata.rb')).cleanpath
78
98
  next unless File.exists? metadata_path
79
99
  md = read_ast(metadata_path)
80
- {'maintainer' => 'YOUR_COMPANY_NAME', 'maintainer_email' => 'YOUR_EMAIL'}.map do |field,value|
81
- md.xpath(%Q{//command[ident/@value='#{field}']/descendant::tstring_content[@value='#{value}']}).map do |m|
100
+ {'maintainer' => 'YOUR_COMPANY_NAME',
101
+ 'maintainer_email' => 'YOUR_EMAIL'}.map do |field,value|
102
+ md.xpath(%Q{//command[ident/@value='#{field}']/
103
+ descendant::tstring_content[@value='#{value}']}).map do |m|
82
104
  match(m).merge(:filename => metadata_path)
83
105
  end
84
106
  end.flatten
@@ -91,12 +113,12 @@ rule "FC009", "Resource attribute not recognised" do
91
113
  matches = []
92
114
  resource_attributes_by_type(ast).each do |type,resources|
93
115
  resources.each do |resource|
94
- resource.keys.map{|att|att.to_sym}.reject do |att|
116
+ resource.keys.map(&:to_sym).reject do |att|
95
117
  resource_attribute?(type.to_sym, att)
96
118
  end.each do |invalid_att|
97
- matches << match(find_resources(ast, :type => type).find do |res|
119
+ matches << find_resources(ast, :type => type).find do |res|
98
120
  resource_attributes(res).include?(invalid_att.to_s)
99
- end)
121
+ end
100
122
  end
101
123
  end
102
124
  end
@@ -108,21 +130,25 @@ rule "FC010", "Invalid search syntax" do
108
130
  tags %w{correctness search}
109
131
  recipe do |ast|
110
132
  # This only works for literal search strings
111
- literal_searches(ast).reject{|search| valid_query?(search['value'])}.map{|search| match(search)}
133
+ literal_searches(ast).reject{|search| valid_query?(search['value'])}
112
134
  end
113
135
  end
114
136
 
115
137
  rule "FC011", "Missing README in markdown format" do
116
138
  tags %w{style readme}
117
139
  cookbook do |filename|
118
- [file_match(File.join(filename, 'README.md'))] unless File.exists?(File.join(filename, 'README.md'))
140
+ unless File.exists?(File.join(filename, 'README.md'))
141
+ [file_match(File.join(filename, 'README.md'))]
142
+ end
119
143
  end
120
144
  end
121
145
 
122
146
  rule "FC012", "Use Markdown for README rather than RDoc" do
123
147
  tags %w{style readme}
124
148
  cookbook do |filename|
125
- [file_match(File.join(filename, 'README.rdoc'))] if File.exists?(File.join(filename, 'README.rdoc'))
149
+ if File.exists?(File.join(filename, 'README.rdoc'))
150
+ [file_match(File.join(filename, 'README.rdoc'))]
151
+ end
126
152
  end
127
153
  end
128
154
 
@@ -130,9 +156,10 @@ rule "FC013", "Use file_cache_path rather than hard-coding tmp paths" do
130
156
  tags %w{style files}
131
157
  recipe do |ast|
132
158
  find_resources(ast, :type => 'remote_file').find_all do |download|
133
- path = (resource_attribute(download, 'path') || resource_name(download)).to_s
159
+ path = (resource_attribute(download, 'path') ||
160
+ resource_name(download)).to_s
134
161
  path.start_with?('/tmp/')
135
- end.map{|download| match(download)}
162
+ end
136
163
  end
137
164
  end
138
165
 
@@ -140,30 +167,38 @@ rule "FC014", "Consider extracting long ruby_block to library" do
140
167
  tags %w{style libraries}
141
168
  recipe do |ast|
142
169
  find_resources(ast, :type => 'ruby_block').find_all do |rb|
143
- ! rb.xpath("//fcall[ident/@value='block' and count(ancestor::*) = 8]/../../do_block[count(descendant::*) > 100]").empty?
144
- end.map{|block| match(block)}
170
+ ! rb.xpath("//fcall[ident/@value='block' and count(ancestor::*) = 8]/../
171
+ ../do_block[count(descendant::*) > 100]").empty?
172
+ end
145
173
  end
146
174
  end
147
175
 
148
176
  rule "FC015", "Consider converting definition to a LWRP" do
149
177
  tags %w{style definitions lwrp}
150
178
  cookbook do |dir|
151
- Dir[File.join(dir, 'definitions', '*.rb')].reject{|entry| ['.', '..'].include? entry}.map{|entry| file_match(entry)}
179
+ Dir[File.join(dir, 'definitions', '*.rb')].reject do |entry|
180
+ ['.', '..'].include? entry
181
+ end.map{|entry| file_match(entry)}
152
182
  end
153
183
  end
154
184
 
155
185
  rule "FC016", "LWRP does not declare a default action" do
156
186
  tags %w{correctness lwrp}
157
187
  resource do |ast, filename|
158
- ast.xpath("//def/bodystmt/descendant::assign/var_field/ivar/@value='@action'") ? [] : [file_match(filename)]
188
+ unless ["//ident/@value='default_action'",
189
+ "//def/bodystmt/descendant::assign/
190
+ var_field/ivar/@value='@action'"].any? {|expr| ast.xpath(expr)}
191
+ [file_match(filename)]
192
+ end
159
193
  end
160
194
  end
161
195
 
162
196
  rule "FC017", "LWRP does not notify when updated" do
163
197
  tags %w{correctness lwrp}
164
198
  provider do |ast, filename|
165
- if ast.xpath(%q{//call/*[self::vcall or self::var_ref/ident/@value='new_resource']/../
166
- ident[@value='updated_by_last_action']}).empty?
199
+ if ast.xpath(%q{//call/*[self::vcall or self::var_ref/ident/
200
+ @value='new_resource']/../
201
+ ident[@value='updated_by_last_action']}).empty?
167
202
  [file_match(filename)]
168
203
  end
169
204
  end
@@ -172,24 +207,34 @@ end
172
207
  rule "FC018", "LWRP uses deprecated notification syntax" do
173
208
  tags %w{style lwrp deprecated}
174
209
  provider do |ast|
175
- ast.xpath("//assign/var_field/ivar[@value='@updated']").map{|class_var| match(class_var)} +
176
- ast.xpath(%q{//assign/field/*[self::vcall or self::var_ref/ident/@value='new_resource']/../
177
- ident[@value='updated']}).map{|assign| match(assign)}
210
+ ast.xpath("//assign/var_field/ivar[@value='@updated']").map do |class_var|
211
+ match(class_var)
212
+ end + ast.xpath(%q{//assign/field/*[self::vcall or self::var_ref/ident/
213
+ @value='new_resource']/../ident[@value='updated']})
178
214
  end
179
215
  end
180
216
 
181
217
  rule "FC019", "Access node attributes in a consistent manner" do
182
218
  tags %w{style attributes}
183
219
  cookbook do |cookbook_dir|
184
- asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].map{|file| {:path => file, :ast => read_ast(file)}}
185
- types = [:string, :symbol, :vivified].map{|type| {:access_type => type, :count => files.map do |file|
186
- attribute_access(file[:ast], :type => type, :ignore_calls => true).tap{|ast|
187
- asts[type] = {:ast => ast, :path => file[:path]} if (! ast.empty?) and (! asts.has_key?(type))
188
- }.size
189
- end.inject(:+)}}.reject{|type| type[:count] == 0}
220
+ asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].map do |file|
221
+ {:path => file, :ast => read_ast(file)}
222
+ end
223
+ types = [:string, :symbol, :vivified].map do |type|
224
+ {:access_type => type, :count => files.map do |file|
225
+ attribute_access(file[:ast], :type => type,
226
+ :ignore_calls => true).tap do |ast|
227
+ if (! ast.empty?) and (! asts.has_key?(type))
228
+ asts[type] = {:ast => ast, :path => file[:path]}
229
+ end
230
+ end.size
231
+ end.inject(:+)}
232
+ end.reject{|type| type[:count] == 0}
190
233
  if asts.size > 1
191
234
  least_used = asts[types.min{|a,b| a[:count] <=> b[:count]}[:access_type]]
192
- least_used[:ast].map{|ast| match(ast).merge(:filename => least_used[:path])}
235
+ least_used[:ast].map do |ast|
236
+ match(ast).merge(:filename => least_used[:path])
237
+ end
193
238
  end
194
239
  end
195
240
  end
@@ -197,13 +242,18 @@ end
197
242
  rule "FC020", "Conditional execution string attribute looks like Ruby" do
198
243
  tags %w{correctness}
199
244
  recipe do |ast, filename|
200
- conditions = ast.xpath(%q{//command[(ident/@value='only_if' or ident/@value='not_if') and
201
- descendant::tstring_content]}).map{|m| match(m)}
245
+ conditions = ast.xpath(%q{//command[(ident/@value='only_if' or ident/
246
+ @value='not_if') and descendant::tstring_content]}).map{|m| match(m)}
202
247
  unless conditions.empty?
203
- lines = File.readlines(filename) # go back and get the raw untokenized string
248
+ lines = File.readlines(filename) # go back for the raw untokenized string
204
249
  conditions.map do |condition|
205
- {:match => condition, :raw_string => lines[(condition[:line].to_i) -1].strip.sub(/^(not|only)_if[\s+]["']/, '').chop}
206
- end.find_all{|cond| ruby_code?(cond[:raw_string]) and ! os_command?(cond[:raw_string])}.map{|cond| cond[:match]}
250
+ line = lines[(condition[:line].to_i) -1]
251
+ {:match => condition,
252
+ :raw_string => line.strip.sub(/^(not|only)_if[\s+]["']/, '').chop}
253
+ end.find_all do |cond|
254
+ ruby_code?(cond[:raw_string]) and
255
+ ! os_command?(cond[:raw_string])
256
+ end.map{|cond| cond[:match]}
207
257
  end
208
258
  end
209
259
  end
@@ -212,10 +262,12 @@ rule "FC021", "Resource condition in provider may not behave as expected" do
212
262
  tags %w{correctness lwrp}
213
263
  provider do |ast|
214
264
  find_resources(ast).map do |resource|
215
- condition = resource.xpath(%q{//method_add_block/descendant::ident[@value='not_if' or @value='only_if']/
216
- ancestor::*[self::method_add_block or self::command][1][descendant::ident/@value='new_resource']/
217
- ancestor::stmts_add[2]/method_add_block/command[count(descendant::string_embexpr) = 0]})
218
- match(condition) unless condition.empty?
265
+ condition = resource.xpath(%q{//method_add_block/
266
+ descendant::ident[@value='not_if' or @value='only_if']/
267
+ ancestor::*[self::method_add_block or self::command][1][descendant::
268
+ ident/@value='new_resource']/ancestor::stmts_add[2]/method_add_block/
269
+ command[count(descendant::string_embexpr) = 0]})
270
+ condition
219
271
  end.compact
220
272
  end
221
273
  end
@@ -224,12 +276,18 @@ rule "FC022", "Resource condition within loop may not behave as expected" do
224
276
  tags %w{correctness}
225
277
  recipe do |ast|
226
278
  ast.xpath("//call[ident/@value='each']/../do_block").map do |loop|
227
- block_vars = loop.xpath("block_var/params/child::*").map{|n| n.name.sub(/^ident/, '')}
279
+ block_vars = loop.xpath("block_var/params/child::*").map do |n|
280
+ n.name.sub(/^ident/, '')
281
+ end
228
282
  find_resources(loop).map do |resource|
229
- # if any of the parameters to the block are used in a condition then we have a match
230
- unless (block_vars & (resource.xpath(%q{descendant::ident[@value='not_if' or @value='only_if']/
231
- ancestor::*[self::method_add_block or self::command][1]/descendant::ident/@value}).map{|a| a.value})).empty?
232
- match(resource) unless resource.xpath('command[count(descendant::string_embexpr) = 0]').empty?
283
+ # if any of the parameters to the block are used in a condition then we
284
+ # have a match
285
+ unless (block_vars &
286
+ (resource.xpath(%q{descendant::ident[@value='not_if' or
287
+ @value='only_if']/ancestor::*[self::method_add_block or
288
+ self::command][1]/descendant::ident/@value}).map{|a| a.value})).empty?
289
+ c = resource.xpath('command[count(descendant::string_embexpr) = 0]')
290
+ resource unless c.empty?
233
291
  end
234
292
  end
235
293
  end.flatten.compact
@@ -243,6 +301,26 @@ rule "FC023", "Prefer conditional attributes" do
243
301
  [@value='only_if' or @value='not_if']) = 0]/ancestor::*[self::if or
244
302
  self::unless][count(descendant::method_add_block[command/ident]) = 1]
245
303
  [count(stmts_add/method_add_block/call) = 0]
246
- [count(stmts_add/stmts_add) = 0]}).map{|condition| match(condition)}
304
+ [count(stmts_add/stmts_add) = 0]})
305
+ end
306
+ end
307
+
308
+ rule "FC024", "Consider adding platform equivalents" do
309
+ tags %w{portability}
310
+ RHEL = %w{amazon centos redhat scientific}
311
+ recipe do |ast|
312
+ ['//method_add_arg[fcall/ident/@value="platform?"]/arg_paren/args_add_block',
313
+ "//when"].map do |expr|
314
+ ast.xpath(expr).map do |whn|
315
+ platforms = whn.xpath("args_add/descendant::tstring_content").map do |p|
316
+ p['value']
317
+ end
318
+ unless platforms.size == 1 || (RHEL & platforms).empty?
319
+ unless (RHEL - platforms).empty?
320
+ whn
321
+ end
322
+ end
323
+ end.compact
324
+ end.flatten
247
325
  end
248
326
  end
@@ -1,4 +1,4 @@
1
1
  module FoodCritic
2
2
  # The current version of foodcritic
3
- VERSION = '1.0.1'
3
+ VERSION = '1.1.0'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foodcritic
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-15 00:00:00.000000000 Z
12
+ date: 2012-03-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: gherkin
16
- requirement: &2158578500 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,15 @@ dependencies:
21
21
  version: 2.8.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2158578500
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.8.0
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: gist
27
- requirement: &2158577320 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ~>
@@ -32,21 +37,43 @@ dependencies:
32
37
  version: 2.0.4
33
38
  type: :runtime
34
39
  prerelease: false
35
- version_requirements: *2158577320
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 2.0.4
36
46
  - !ruby/object:Gem::Dependency
37
47
  name: nokogiri
38
- requirement: &2158576420 !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
39
49
  none: false
40
50
  requirements:
41
- - - =
51
+ - - ~>
42
52
  - !ruby/object:Gem::Version
43
53
  version: 1.5.0
54
+ - - ! '!='
55
+ - !ruby/object:Gem::Version
56
+ version: 1.5.1
57
+ - - ! '!='
58
+ - !ruby/object:Gem::Version
59
+ version: 1.5.2
44
60
  type: :runtime
45
61
  prerelease: false
46
- version_requirements: *2158576420
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: 1.5.0
68
+ - - ! '!='
69
+ - !ruby/object:Gem::Version
70
+ version: 1.5.1
71
+ - - ! '!='
72
+ - !ruby/object:Gem::Version
73
+ version: 1.5.2
47
74
  - !ruby/object:Gem::Dependency
48
75
  name: pry
49
- requirement: &2158592160 !ruby/object:Gem::Requirement
76
+ requirement: !ruby/object:Gem::Requirement
50
77
  none: false
51
78
  requirements:
52
79
  - - ~>
@@ -54,10 +81,15 @@ dependencies:
54
81
  version: 0.9.7.4
55
82
  type: :runtime
56
83
  prerelease: false
57
- version_requirements: *2158592160
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 0.9.7.4
58
90
  - !ruby/object:Gem::Dependency
59
91
  name: pry-doc
60
- requirement: &2158591020 !ruby/object:Gem::Requirement
92
+ requirement: !ruby/object:Gem::Requirement
61
93
  none: false
62
94
  requirements:
63
95
  - - ~>
@@ -65,10 +97,15 @@ dependencies:
65
97
  version: 0.3.0
66
98
  type: :runtime
67
99
  prerelease: false
68
- version_requirements: *2158591020
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ~>
104
+ - !ruby/object:Gem::Version
105
+ version: 0.3.0
69
106
  - !ruby/object:Gem::Dependency
70
107
  name: rak
71
- requirement: &2158589520 !ruby/object:Gem::Requirement
108
+ requirement: !ruby/object:Gem::Requirement
72
109
  none: false
73
110
  requirements:
74
111
  - - ~>
@@ -76,10 +113,15 @@ dependencies:
76
113
  version: '1.4'
77
114
  type: :runtime
78
115
  prerelease: false
79
- version_requirements: *2158589520
116
+ version_requirements: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ~>
120
+ - !ruby/object:Gem::Version
121
+ version: '1.4'
80
122
  - !ruby/object:Gem::Dependency
81
123
  name: treetop
82
- requirement: &2158587600 !ruby/object:Gem::Requirement
124
+ requirement: !ruby/object:Gem::Requirement
83
125
  none: false
84
126
  requirements:
85
127
  - - ~>
@@ -87,10 +129,15 @@ dependencies:
87
129
  version: 1.4.10
88
130
  type: :runtime
89
131
  prerelease: false
90
- version_requirements: *2158587600
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ~>
136
+ - !ruby/object:Gem::Version
137
+ version: 1.4.10
91
138
  - !ruby/object:Gem::Dependency
92
139
  name: yajl-ruby
93
- requirement: &2158585060 !ruby/object:Gem::Requirement
140
+ requirement: !ruby/object:Gem::Requirement
94
141
  none: false
95
142
  requirements:
96
143
  - - ~>
@@ -98,7 +145,12 @@ dependencies:
98
145
  version: 1.1.0
99
146
  type: :runtime
100
147
  prerelease: false
101
- version_requirements: *2158585060
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ~>
152
+ - !ruby/object:Gem::Version
153
+ version: 1.1.0
102
154
  description: Lint tool for Opscode Chef cookbooks.
103
155
  email:
104
156
  executables:
@@ -118,7 +170,8 @@ files:
118
170
  - lib/foodcritic/version.rb
119
171
  - lib/foodcritic.rb
120
172
  - chef_dsl_metadata.json
121
- - bin/foodcritic
173
+ - !binary |-
174
+ YmluL2Zvb2Rjcml0aWM=
122
175
  homepage: http://acrmp.github.com/foodcritic
123
176
  licenses:
124
177
  - MIT
@@ -140,12 +193,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
193
  version: '0'
141
194
  segments:
142
195
  - 0
143
- hash: -1104674546192512952
196
+ hash: -4140544099475579299
144
197
  requirements: []
145
198
  rubyforge_project:
146
- rubygems_version: 1.8.10
199
+ rubygems_version: 1.8.19
147
200
  signing_key:
148
201
  specification_version: 3
149
- summary: foodcritic-1.0.1
202
+ summary: foodcritic-1.1.0
150
203
  test_files: []
151
204
  has_rdoc: