foodcritic 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: