foodcritic 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/foodcritic CHANGED
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  require_relative '../lib/foodcritic'
3
- result, status = FoodCritic::Linter.check(ARGV)
4
- puts result; exit status.to_i
3
+ module FoodCritic
4
+ cmd_line = CommandLine.new(ARGV)
5
+ review, status = Linter.check(cmd_line)
6
+ printer = cmd_line.show_context? ? ContextOutput.new : SummaryOutput.new
7
+ printer.output(review)
8
+ exit status.to_i
9
+ end
data/lib/foodcritic.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  require 'chef'
2
2
  require 'pry'
3
+ require 'rak'
4
+ require_relative 'foodcritic/command_line'
3
5
  require_relative 'foodcritic/domain'
4
6
  require_relative 'foodcritic/error_checker'
5
7
  require_relative 'foodcritic/helpers'
6
8
  require_relative 'foodcritic/dsl'
7
9
  require_relative 'foodcritic/linter'
10
+ require_relative 'foodcritic/output'
8
11
  require_relative 'foodcritic/version'
@@ -0,0 +1,67 @@
1
+ module FoodCritic
2
+
3
+ # Command line parsing.
4
+ class CommandLine
5
+
6
+ # Create a new instance of CommandLine
7
+ #
8
+ # @param [Array] args The command line arguments
9
+ def initialize(args)
10
+ @args = args
11
+ @options = {}
12
+ @options[:fail_tags] = []; @options[:tags] = []
13
+ @parser = OptionParser.new do |opts|
14
+ opts.banner = 'foodcritic [cookbook_path]'
15
+ opts.on("-r", "--[no-]repl", "Drop into a REPL for interactive rule editing.") {|r|options[:repl] = r}
16
+ opts.on("-t", "--tags TAGS", "Only check against rules with the specified tags.") {|t|options[:tags] << t}
17
+ opts.on("-f", "--epic-fail TAGS", "Fail the build if any of the specified tags are matched.") {|t|options[:fail_tags] << t}
18
+ opts.on("-C", "--[no-]context", "Show lines matched against rather than the default summary.") {|c|options[:context] = c}
19
+ end
20
+ @parser.parse!(args) unless show_help?
21
+ end
22
+
23
+ # Show the command help to the end user?
24
+ #
25
+ # @return [Boolean] True if help should be shown.
26
+ def show_help?
27
+ @args.length == 1 and @args.first == '--help'
28
+ end
29
+
30
+ # The help text.
31
+ #
32
+ # @return [String] Help text describing the command-line options available.
33
+ def help
34
+ @parser.help
35
+ end
36
+
37
+ # If the cookbook path provided is valid
38
+ #
39
+ # @return [Boolean] True if the path is a directory that exists.
40
+ def valid_path?
41
+ @args.length == 1 and Dir.exists?(@args[0])
42
+ end
43
+
44
+ # The cookbook path
45
+ #
46
+ # @return [String] Path to the cookbook(s) being checked.
47
+ def cookbook_path
48
+ @args[0]
49
+ end
50
+
51
+ # If matches should be shown with context rather than the default summary display.
52
+ #
53
+ # @return [Boolean] True if matches should be shown with context.
54
+ def show_context?
55
+ @options[:context]
56
+ end
57
+
58
+ # Parsed command-line options
59
+ #
60
+ # @return [Hash] The parsed command-line options.
61
+ def options
62
+ @options
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -19,13 +19,15 @@ module FoodCritic
19
19
  # The collected warnings (if any) raised against a cookbook tree.
20
20
  class Review
21
21
 
22
- attr_reader :warnings
22
+ attr_reader :cookbook_path, :warnings
23
23
 
24
24
  # Create a new review
25
25
  #
26
+ # @param [String] cookbook_path The path this review was performed against
26
27
  # @param [Array] warnings The warnings raised in this review
27
28
  # @param [Boolean] is_failed Have warnings been raised that mean this should be considered failed?
28
- def initialize(warnings, is_failed)
29
+ def initialize(cookbook_path, warnings, is_failed)
30
+ @cookbook_path = cookbook_path
29
31
  @warnings = warnings
30
32
  @is_failed = is_failed
31
33
  end
@@ -49,7 +51,7 @@ module FoodCritic
49
51
 
50
52
  # A rule to be matched against.
51
53
  class Rule
52
- attr_accessor :code, :name, :cookbook, :recipe, :provider, :tags
54
+ attr_accessor :code, :name, :cookbook, :recipe, :provider, :resource, :tags
53
55
 
54
56
  # Create a new rule
55
57
  #
@@ -34,6 +34,13 @@ module FoodCritic
34
34
  rules.last.recipe = block
35
35
  end
36
36
 
37
+ # Define a matcher that will be passed the AST with this method.
38
+ #
39
+ # @param [block] block Your implemented matcher that returns a match Hash.
40
+ def resource(&block)
41
+ rules.last.resource = block
42
+ end
43
+
37
44
  # Define a matcher that will be passed the AST with this method.
38
45
  #
39
46
  # @param [block] block Your implemented matcher that returns a match Hash.
@@ -85,11 +85,13 @@ module FoodCritic
85
85
  def attribute_access(ast, accessed_via, exclude_with_dots)
86
86
  %w{node default override set normal}.map do |att_type|
87
87
  if accessed_via == :vivified
88
- call = ast.xpath(%Q{//*[self::call or self::field][vcall/ident/@value='#{att_type}' or
88
+ calls = ast.xpath(%Q{//*[self::call or self::field][vcall/ident/@value='#{att_type}' or
89
89
  var_ref/ident/@value='#{att_type}'][@value='.']})
90
- call.xpath("aref/args_add_block").size == 0 and (call.xpath("descendant::ident").size > 1 and
91
- call.xpath("descendant::ident").first['value'] == 'node' and
92
- ! Chef::Node.public_instance_methods.include?(call.xpath("ident/@value").to_s.to_sym)) ? call : []
90
+ calls.select do |call|
91
+ call.xpath("aref/args_add_block").size == 0 and (call.xpath("descendant::ident").size > 1 and
92
+ call.xpath("descendant::ident").first['value'] == att_type.to_s and
93
+ ! dsl_methods.include?(call.xpath("ident/@value").to_s.to_sym))
94
+ end
93
95
  else
94
96
  accessed_via = 'tstring_content' if accessed_via == :string
95
97
  expr = '//*[self::aref_field or self::aref][descendant::ident'
@@ -100,6 +102,14 @@ module FoodCritic
100
102
  end.flatten.sort
101
103
  end
102
104
 
105
+ # The set of methods in the Chef DSL
106
+ #
107
+ # @return [Array] Array of method symbols
108
+ def dsl_methods
109
+ (Chef::Node.public_instance_methods +
110
+ Chef::Mixin::RecipeDefinitionDSLCore.included_modules.map{|mixin| mixin.public_instance_methods}).flatten.sort.uniq
111
+ end
112
+
103
113
  # Find Chef resources of the specified type.
104
114
  # TODO: Include blockless resources
105
115
  #
@@ -13,25 +13,13 @@ module FoodCritic
13
13
  #
14
14
  # @param [Array] args The command-line arguments to parse
15
15
  # @return [Array] Pair - the first item is string output, the second is the exit code.
16
- def self.check(args)
17
- options = {}
18
- options[:fail_tags] = []; options[:tags] = []
19
- parser = OptionParser.new do |opts|
20
- opts.banner = 'foodcritic [cookbook_path]'
21
- opts.on("-r", "--[no-]repl", "Drop into a REPL for interactive rule editing.") {|r|options[:repl] = r}
22
- opts.on("-t", "--tags TAGS", "Only check against rules with the specified tags.") {|t|options[:tags] << t}
23
- opts.on("-f", "--epic-fail TAGS", "Fail the build if any of the specified tags are matched.") {|t|options[:fail_tags] << t}
24
- end
25
-
26
- return [parser.help, 0] if args.length == 1 and args.first == '--help'
27
-
28
- parser.parse!(args)
29
-
30
- if args.length == 1 and Dir.exists?(args[0])
31
- review = FoodCritic::Linter.new.check(args[0], options)
16
+ def self.check(cmd_line)
17
+ return [cmd_line.help, 0] if cmd_line.show_help?
18
+ if cmd_line.valid_path?
19
+ review = FoodCritic::Linter.new.check(cmd_line.cookbook_path, cmd_line.options)
32
20
  [review, review.failed? ? 3 : 0]
33
21
  else
34
- [parser.help, 2]
22
+ [cmd_line.help, 2]
35
23
  end
36
24
  end
37
25
 
@@ -59,6 +47,7 @@ module FoodCritic
59
47
  @rules.select{|rule| tag_expr.eval(rule.tags)}.each do |rule|
60
48
  rule_matches = matches(rule.recipe, ast, file)
61
49
  rule_matches += matches(rule.provider, ast, file) if File.basename(File.dirname(file)) == 'providers'
50
+ rule_matches += matches(rule.resource, ast, file) if File.basename(File.dirname(file)) == 'resources'
62
51
  rule_matches += matches(rule.cookbook, cookbook_dir) if last_dir != cookbook_dir
63
52
  rule_matches.each do |match|
64
53
  warnings << Warning.new(rule, {:filename => file}.merge(match))
@@ -68,7 +57,7 @@ module FoodCritic
68
57
  last_dir = cookbook_dir
69
58
  end
70
59
 
71
- @review = Review.new(warnings, should_fail_build?(options[:fail_tags], matched_rule_tags))
60
+ @review = Review.new(cookbook_path, warnings, should_fail_build?(options[:fail_tags], matched_rule_tags))
72
61
 
73
62
  binding.pry if options[:repl]
74
63
  @review
@@ -112,8 +101,8 @@ module FoodCritic
112
101
  # @return [Array] The files underneath the provided directory to be processed.
113
102
  def files_to_process(dir)
114
103
  return [dir] unless File.directory? dir
115
- Dir.glob(File.join(dir, '{attributes,providers,recipes}/*.rb')) +
116
- Dir.glob(File.join(dir, '*/{attributes,providers,recipes}/*.rb'))
104
+ Dir.glob(File.join(dir, '{attributes,providers,recipes,resources}/*.rb')) +
105
+ Dir.glob(File.join(dir, '*/{attributes,providers,recipes,resources}/*.rb'))
117
106
  end
118
107
 
119
108
  # Whether to fail the build.
@@ -0,0 +1,70 @@
1
+ module FoodCritic
2
+
3
+ # Default output showing a summary view.
4
+ class SummaryOutput
5
+ # Output a summary view only listing the matching rules, file and line number.
6
+ #
7
+ # @param [Review] review The review to output.
8
+ def output(review)
9
+ puts review.to_s
10
+ end
11
+ end
12
+
13
+ # Display rule matches with surrounding context.
14
+ class ContextOutput
15
+
16
+ # Output the review showing matching lines with context.
17
+ #
18
+ # @param [Review] review The review to output.
19
+ def output(review)
20
+ unless review.respond_to?(:warnings)
21
+ puts review; return
22
+ end
23
+
24
+ # Cheating here and mis-using Rak (Ruby port of Ack) to generate pretty colourised context.
25
+ #
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
+ line_lookup = key_by_file_and_line(review)
29
+ Rak.class_eval do
30
+ const_set(:RULE_COLOUR, "\033[1;36m")
31
+ @warnings = line_lookup
32
+ end
33
+ ARGV.replace(['--context', '--eval', %q{
34
+ # This code will be evaluated inline by Rak.
35
+ 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}"
38
+ if ! displayed_filename
39
+ fn = "#{fn}\n#{rule_name}"
40
+ else
41
+ puts rule_name
42
+ end
43
+ else
44
+ next
45
+ end
46
+ }, review.cookbook_path])
47
+ Rak.send(:remove_const, :VERSION) # Prevent duplicate VERSION warning
48
+ load Gem.bin_path('rak', 'rak') # Assumes Rubygems
49
+ end
50
+
51
+ private
52
+
53
+ # Build a hash lookup by filename and line number for warnings found in the specified review.
54
+ #
55
+ # @param [Review] review The review to convert.
56
+ # @return [Hash] Nested hashes keyed by filename and line number.
57
+ def key_by_file_and_line(review)
58
+ warn_hash = {}
59
+ review.warnings.each do |warning|
60
+ filename = Pathname.new(warning.match[:filename]).cleanpath.to_s; line_num = warning.match[:line].to_i
61
+ warn_hash[filename] = {} unless warn_hash.key?(filename)
62
+ warn_hash[filename][line_num] = Set.new unless warn_hash[filename].key?(line_num)
63
+ warn_hash[filename][line_num] << warning.rule
64
+ end
65
+ warn_hash
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -34,15 +34,14 @@ end
34
34
  rule "FC005", "Avoid repetition of resource declarations" do
35
35
  tags %w{style}
36
36
  recipe do |ast|
37
- matches = []
38
- # do all of the attributes for all resources of a given type match apart aside from one?
39
- resource_attributes_by_type(ast).each do |type, resource_atts|
40
- sorted_atts = resource_atts.map{|atts| atts.to_a.sort{|x,y| x.first.to_s <=> y.first.to_s }}
41
- if sorted_atts.length > 2 and sorted_atts.all?{|att| (att - sorted_atts.inject{|atts,a| atts & a}).length == 1}
42
- matches << match(find_resources(ast, type).first)
43
- end
44
- end
45
- matches
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}
39
+ resources.map do |cont_res|
40
+ 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}
44
+ end.compact
46
45
  end
47
46
  end
48
47
 
@@ -148,6 +147,13 @@ rule "FC015", "Consider converting definition to a LWRP" do
148
147
  end
149
148
  end
150
149
 
150
+ rule "FC016", "LWRP does not declare a default action" do
151
+ tags %w{correctness lwrp}
152
+ resource do |ast, filename|
153
+ ast.xpath("//def/bodystmt/descendant::assign/var_field/ivar/@value='@action'") ? [] : [file_match(filename)]
154
+ end
155
+ end
156
+
151
157
  rule "FC017", "LWRP does not notify when updated" do
152
158
  tags %w{correctness lwrp}
153
159
  provider do |ast, filename|
@@ -171,14 +177,14 @@ rule "FC019", "Access node attributes in a consistent manner" do
171
177
  tags %w{style attributes}
172
178
  cookbook do |cookbook_dir|
173
179
  asts = {}; files = Dir["#{cookbook_dir}/**/*.rb"].map{|file| {:path => file, :ast => read_file(file)}}
174
- types = [:string, :symbol, :vivified].map{|type| {:access_type => type, :count => files.count do |file|
175
- ! attribute_access(file[:ast], type, true).tap{|ast|
176
- asts[type] = {:ast => ast.first, :path => file[:path]} if (! ast.empty?) and (! asts.has_key?(type))
177
- }.empty?
178
- end}}.reject{|type| type[:count] == 0}
180
+ types = [:string, :symbol, :vivified].map{|type| {:access_type => type, :count => files.map do |file|
181
+ attribute_access(file[:ast], type, true).tap{|ast|
182
+ asts[type] = {:ast => ast, :path => file[:path]} if (! ast.empty?) and (! asts.has_key?(type))
183
+ }.size
184
+ end.inject(:+)}}.reject{|type| type[:count] == 0}
179
185
  if asts.size > 1
180
186
  least_used = asts[types.min{|a,b| a[:count] <=> b[:count]}[:access_type]]
181
- [match(least_used[:ast]).merge(:filename => least_used[:path])]
187
+ least_used[:ast].map{|ast| match(ast).merge(:filename => least_used[:path])}
182
188
  end
183
189
  end
184
190
  end
@@ -191,8 +197,36 @@ rule "FC020", "Conditional execution string attribute looks like Ruby" do
191
197
  unless conditions.empty?
192
198
  lines = File.readlines(filename) # go back and get the raw untokenized string
193
199
  conditions.map do |condition|
194
- {:match => condition, :raw_string => lines[(condition[:line].to_i) -1].strip.sub(/^(not|only)_if[\s+]"/, '').chop}
200
+ {:match => condition, :raw_string => lines[(condition[:line].to_i) -1].strip.sub(/^(not|only)_if[\s+]["']/, '').chop}
195
201
  end.find_all{|cond| ruby_code?(cond[:raw_string]) and ! os_command?(cond[:raw_string])}.map{|cond| cond[:match]}
196
202
  end
197
203
  end
204
+ end
205
+
206
+ rule "FC021", "Resource condition in provider may not behave as expected" do
207
+ tags %w{correctness lwrp}
208
+ provider do |ast|
209
+ find_resources(ast).map do |resource|
210
+ condition = resource.xpath(%q{//method_add_block/descendant::ident[@value='not_if' or @value='only_if']/
211
+ ancestor::*[self::method_add_block or self::command][1][descendant::ident/@value='new_resource']/
212
+ ancestor::stmts_add[2]/method_add_block/command[count(descendant::string_embexpr) = 0]})
213
+ match(condition) unless condition.empty?
214
+ end.compact
215
+ end
216
+ end
217
+
218
+ rule "FC022", "Resource condition within loop may not behave as expected" do
219
+ tags %w{correctness}
220
+ recipe do |ast|
221
+ ast.xpath("//call[ident/@value='each']/../do_block").map do |loop|
222
+ block_vars = loop.xpath("block_var/params/child::*").map{|n| n.name.sub(/^ident/, '')}
223
+ find_resources(loop).map do |resource|
224
+ # if any of the parameters to the block are used in a condition then we have a match
225
+ unless (block_vars & (resource.xpath(%q{descendant::ident[@value='not_if' or @value='only_if']/
226
+ ancestor::*[self::method_add_block or self::command][1]/descendant::ident/@value}).map{|a| a.value})).empty?
227
+ match(resource) unless resource.xpath('command[count(descendant::string_embexpr) = 0]').empty?
228
+ end
229
+ end
230
+ end.flatten.compact
231
+ end
198
232
  end
@@ -1,4 +1,4 @@
1
1
  module FoodCritic
2
2
  # The current version of foodcritic
3
- VERSION = '0.8.1'
3
+ VERSION = '0.9.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: 0.8.1
4
+ version: 0.9.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-01-20 00:00:00.000000000 Z
12
+ date: 2012-01-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: chef
16
- requirement: &2152165640 !ruby/object:Gem::Requirement
16
+ requirement: &2156685440 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.10.4
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2152165640
24
+ version_requirements: *2156685440
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: json
27
- requirement: &2152164620 !ruby/object:Gem::Requirement
27
+ requirement: &2156705560 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -35,10 +35,10 @@ dependencies:
35
35
  version: 1.6.1
36
36
  type: :runtime
37
37
  prerelease: false
38
- version_requirements: *2152164620
38
+ version_requirements: *2156705560
39
39
  - !ruby/object:Gem::Dependency
40
40
  name: gherkin
41
- requirement: &2152022200 !ruby/object:Gem::Requirement
41
+ requirement: &2156702700 !ruby/object:Gem::Requirement
42
42
  none: false
43
43
  requirements:
44
44
  - - ~>
@@ -46,10 +46,10 @@ dependencies:
46
46
  version: 2.7.1
47
47
  type: :runtime
48
48
  prerelease: false
49
- version_requirements: *2152022200
49
+ version_requirements: *2156702700
50
50
  - !ruby/object:Gem::Dependency
51
51
  name: gist
52
- requirement: &2152020740 !ruby/object:Gem::Requirement
52
+ requirement: &2156700200 !ruby/object:Gem::Requirement
53
53
  none: false
54
54
  requirements:
55
55
  - - ~>
@@ -57,10 +57,10 @@ dependencies:
57
57
  version: 2.0.4
58
58
  type: :runtime
59
59
  prerelease: false
60
- version_requirements: *2152020740
60
+ version_requirements: *2156700200
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: nokogiri
63
- requirement: &2152019320 !ruby/object:Gem::Requirement
63
+ requirement: &2156712640 !ruby/object:Gem::Requirement
64
64
  none: false
65
65
  requirements:
66
66
  - - ~>
@@ -68,10 +68,10 @@ dependencies:
68
68
  version: 1.5.0
69
69
  type: :runtime
70
70
  prerelease: false
71
- version_requirements: *2152019320
71
+ version_requirements: *2156712640
72
72
  - !ruby/object:Gem::Dependency
73
73
  name: pry
74
- requirement: &2152016360 !ruby/object:Gem::Requirement
74
+ requirement: &2156708060 !ruby/object:Gem::Requirement
75
75
  none: false
76
76
  requirements:
77
77
  - - ~>
@@ -79,10 +79,10 @@ dependencies:
79
79
  version: 0.9.7.4
80
80
  type: :runtime
81
81
  prerelease: false
82
- version_requirements: *2152016360
82
+ version_requirements: *2156708060
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: pry-doc
85
- requirement: &2152015400 !ruby/object:Gem::Requirement
85
+ requirement: &2156721400 !ruby/object:Gem::Requirement
86
86
  none: false
87
87
  requirements:
88
88
  - - ~>
@@ -90,7 +90,18 @@ dependencies:
90
90
  version: 0.3.0
91
91
  type: :runtime
92
92
  prerelease: false
93
- version_requirements: *2152015400
93
+ version_requirements: *2156721400
94
+ - !ruby/object:Gem::Dependency
95
+ name: rak
96
+ requirement: &2156718720 !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '1.4'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: *2156718720
94
105
  description: Lint tool for Opscode Chef cookbooks.
95
106
  email:
96
107
  executables:
@@ -98,11 +109,13 @@ executables:
98
109
  extensions: []
99
110
  extra_rdoc_files: []
100
111
  files:
112
+ - lib/foodcritic/command_line.rb
101
113
  - lib/foodcritic/domain.rb
102
114
  - lib/foodcritic/dsl.rb
103
115
  - lib/foodcritic/error_checker.rb
104
116
  - lib/foodcritic/helpers.rb
105
117
  - lib/foodcritic/linter.rb
118
+ - lib/foodcritic/output.rb
106
119
  - lib/foodcritic/rules.rb
107
120
  - lib/foodcritic/version.rb
108
121
  - lib/foodcritic.rb
@@ -128,12 +141,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
141
  version: '0'
129
142
  segments:
130
143
  - 0
131
- hash: -295625784771858454
144
+ hash: -4091807695676040687
132
145
  requirements: []
133
146
  rubyforge_project:
134
147
  rubygems_version: 1.8.10
135
148
  signing_key:
136
149
  specification_version: 3
137
- summary: foodcritic-0.8.1
150
+ summary: foodcritic-0.9.0
138
151
  test_files: []
139
152
  has_rdoc: