foodcritic 0.6.0 → 0.7.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.
- data/bin/foodcritic +2 -17
- data/lib/foodcritic.rb +1 -0
- data/lib/foodcritic/domain.rb +18 -2
- data/lib/foodcritic/dsl.rb +3 -8
- data/lib/foodcritic/helpers.rb +18 -1
- data/lib/foodcritic/linter.rb +77 -10
- data/lib/foodcritic/rules.rb +3 -13
- data/lib/foodcritic/version.rb +2 -1
- metadata +45 -12
data/bin/foodcritic
CHANGED
@@ -1,19 +1,4 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'foodcritic'
|
3
|
-
|
4
|
-
|
5
|
-
options = {}
|
6
|
-
options[:tags] = []
|
7
|
-
parser = OptionParser.new do |opts|
|
8
|
-
opts.banner = 'foodcritic [cookbook_path]'
|
9
|
-
opts.on("-t", "--tags TAGS", "Only check against rules with the specified tags.") {|t|options[:tags] << t}
|
10
|
-
end
|
11
|
-
parser.parse!
|
12
|
-
|
13
|
-
unless ARGV.length == 1 and Dir.exists?(ARGV[0])
|
14
|
-
puts parser.help
|
15
|
-
exit 1
|
16
|
-
end
|
17
|
-
|
18
|
-
review = FoodCritic::Linter.new.check(ARGV[0], options)
|
19
|
-
puts review unless review.warnings.empty?
|
3
|
+
result, status = FoodCritic::Linter.check(ARGV)
|
4
|
+
puts result; exit status.to_i
|
data/lib/foodcritic.rb
CHANGED
data/lib/foodcritic/domain.rb
CHANGED
@@ -24,8 +24,17 @@ module FoodCritic
|
|
24
24
|
# Create a new review
|
25
25
|
#
|
26
26
|
# @param [Array] warnings The warnings raised in this review
|
27
|
-
|
27
|
+
# @param [Boolean] is_failed Have warnings been raised that mean this should be considered failed?
|
28
|
+
def initialize(warnings, is_failed)
|
28
29
|
@warnings = warnings
|
30
|
+
@is_failed = is_failed
|
31
|
+
end
|
32
|
+
|
33
|
+
# If this review has failed or not.
|
34
|
+
#
|
35
|
+
# @return [Boolean] True if this review has failed.
|
36
|
+
def failed?
|
37
|
+
@is_failed
|
29
38
|
end
|
30
39
|
|
31
40
|
# Returns a string representation of this review.
|
@@ -40,7 +49,7 @@ module FoodCritic
|
|
40
49
|
|
41
50
|
# A rule to be matched against.
|
42
51
|
class Rule
|
43
|
-
attr_accessor :code, :name, :
|
52
|
+
attr_accessor :code, :name, :cookbook, :recipe, :provider, :tags
|
44
53
|
|
45
54
|
# Create a new rule
|
46
55
|
#
|
@@ -50,6 +59,13 @@ module FoodCritic
|
|
50
59
|
@code, @name = code, name
|
51
60
|
@tags = [code]
|
52
61
|
end
|
62
|
+
|
63
|
+
# Returns a string representation of this rule.
|
64
|
+
#
|
65
|
+
# @return [String] Rule as a string.
|
66
|
+
def to_s
|
67
|
+
"#{@code}: #{@name}"
|
68
|
+
end
|
53
69
|
end
|
54
70
|
|
55
71
|
end
|
data/lib/foodcritic/dsl.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'pathname'
|
2
2
|
|
3
|
+
# FoodCritic is a lint tool for Chef cookbooks.
|
3
4
|
module FoodCritic
|
4
5
|
|
5
6
|
# The DSL methods exposed for defining rules.
|
@@ -26,13 +27,6 @@ module FoodCritic
|
|
26
27
|
rules.last.tags += tags
|
27
28
|
end
|
28
29
|
|
29
|
-
# Set the rule description
|
30
|
-
#
|
31
|
-
# @param [String] description Set the rule description.
|
32
|
-
def description(description)
|
33
|
-
rules.last.description = description
|
34
|
-
end
|
35
|
-
|
36
30
|
# Define a matcher that will be passed the AST with this method.
|
37
31
|
#
|
38
32
|
# @param [block] block Your implemented matcher that returns a match Hash.
|
@@ -58,9 +52,10 @@ module FoodCritic
|
|
58
52
|
#
|
59
53
|
# @param [String] filename The path to the ruleset to load
|
60
54
|
# @return [Array] The loaded rules, ready to be matched against provided cookbooks.
|
61
|
-
def self.load(filename)
|
55
|
+
def self.load(filename, with_repl)
|
62
56
|
dsl = RuleDsl.new
|
63
57
|
dsl.instance_eval(File.read(filename), filename)
|
58
|
+
dsl.instance_eval { binding.pry } if with_repl
|
64
59
|
dsl.rules
|
65
60
|
end
|
66
61
|
end
|
data/lib/foodcritic/helpers.rb
CHANGED
@@ -15,6 +15,11 @@ module FoodCritic
|
|
15
15
|
{:matched => node.respond_to?(:name) ? node.name : '', :line => pos['line'], :column => pos['column']}
|
16
16
|
end
|
17
17
|
|
18
|
+
# Create a match for a specified file. Use this if the presence of the file triggers the warning rather than content.
|
19
|
+
#
|
20
|
+
# @param [String] file The filename to create a match for
|
21
|
+
# @return [Hash] Hash with the match details
|
22
|
+
# @see FoodCritic::Helpers#match
|
18
23
|
def file_match(file)
|
19
24
|
{:filename => file, :matched => file, :line => 1, :column => 1}
|
20
25
|
end
|
@@ -28,6 +33,18 @@ module FoodCritic
|
|
28
33
|
count(descendant::ident[@value='solo']) > 0]}).empty?
|
29
34
|
end
|
30
35
|
|
36
|
+
# Is the chef-solo-search library available?
|
37
|
+
#
|
38
|
+
# @param [String] recipe_path The path to the current recipe
|
39
|
+
# @return [Boolean] True if the chef-solo-search library is available.
|
40
|
+
def chef_solo_search_supported?(recipe_path)
|
41
|
+
search_libs = Dir[File.join(Pathname.new(File.join(recipe_path, '../../..')).realpath, "**/libraries/search.rb")]
|
42
|
+
search_libs.any? do |lib|
|
43
|
+
! read_file(lib).xpath(%q{//class[count(descendant::const[@value='Chef' or @value='Recipe']) = 2]/
|
44
|
+
descendant::def/ident[@value='search']}).empty?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
31
48
|
# Searches performed by the specified recipe.
|
32
49
|
#
|
33
50
|
# @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
|
@@ -173,7 +190,7 @@ module FoodCritic
|
|
173
190
|
|
174
191
|
# Recurse the nested arrays provided by Ripper to create a tree we can more easily apply expressions to.
|
175
192
|
#
|
176
|
-
# @param [
|
193
|
+
# @param [Array] node The AST
|
177
194
|
# @param [Nokogiri::XML::Document] doc The document being constructed
|
178
195
|
# @param [Nokogiri::XML::Node] xml_node The current node
|
179
196
|
# @return [Nokogiri::XML::Node] The XML representation
|
data/lib/foodcritic/linter.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'optparse'
|
1
2
|
require 'ripper'
|
2
3
|
require 'gherkin/tag_expression'
|
3
4
|
|
@@ -8,9 +9,35 @@ module FoodCritic
|
|
8
9
|
|
9
10
|
include FoodCritic::Helpers
|
10
11
|
|
11
|
-
#
|
12
|
+
# Perform option parsing from the provided arguments and do a lint check based on those arguments.
|
13
|
+
#
|
14
|
+
# @param [Array] args The command-line arguments to parse
|
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)
|
32
|
+
[review, review.failed? ? 3 : 0]
|
33
|
+
else
|
34
|
+
[parser.help, 2]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create a new Linter.
|
12
39
|
def initialize
|
13
|
-
|
40
|
+
|
14
41
|
end
|
15
42
|
|
16
43
|
# Review the cookbooks at the provided path, identifying potential improvements.
|
@@ -18,10 +45,14 @@ module FoodCritic
|
|
18
45
|
# @param [String] cookbook_path The file path to an individual cookbook directory
|
19
46
|
# @param [Hash] options Options to apply to the linting
|
20
47
|
# @option options [Array] tags The tags to filter rules based on
|
48
|
+
# @option options [Array] fail_tags The tags to fail the build on
|
21
49
|
# @return [FoodCritic::Review] A review of your cookbooks, with any warnings issued.
|
22
50
|
def check(cookbook_path, options)
|
23
|
-
|
51
|
+
@last_cookbook_path, @last_options = cookbook_path, options
|
52
|
+
load_rules unless defined? @rules
|
53
|
+
warnings = []; last_dir = nil; matched_rule_tags = Set.new
|
24
54
|
tag_expr = Gherkin::TagExpression.new(options[:tags])
|
55
|
+
|
25
56
|
files_to_process(cookbook_path).each do |file|
|
26
57
|
cookbook_dir = Pathname.new(File.join(File.dirname(file), '..')).cleanpath
|
27
58
|
ast = read_file(file)
|
@@ -29,11 +60,37 @@ module FoodCritic
|
|
29
60
|
rule_matches = matches(rule.recipe, ast, file)
|
30
61
|
rule_matches += matches(rule.provider, ast, file) if File.basename(File.dirname(file)) == 'providers'
|
31
62
|
rule_matches += matches(rule.cookbook, cookbook_dir) if last_dir != cookbook_dir
|
32
|
-
rule_matches.each
|
63
|
+
rule_matches.each do |match|
|
64
|
+
warnings << Warning.new(rule, {:filename => file}.merge(match))
|
65
|
+
matched_rule_tags += rule.tags
|
66
|
+
end
|
33
67
|
end
|
34
68
|
last_dir = cookbook_dir
|
35
69
|
end
|
36
|
-
|
70
|
+
|
71
|
+
@review = Review.new(warnings, should_fail_build?(options[:fail_tags], matched_rule_tags))
|
72
|
+
|
73
|
+
binding.pry if options[:repl]
|
74
|
+
@review
|
75
|
+
end
|
76
|
+
|
77
|
+
# Convenience method to repeat the last check. Intended to be used from the REPL.
|
78
|
+
def recheck
|
79
|
+
check(@last_cookbook_path, @last_options)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Load the rules from the (fairly unnecessary) DSL.
|
83
|
+
def load_rules
|
84
|
+
@rules = RuleDsl.load(File.join(File.dirname(__FILE__), 'rules.rb'), @last_options[:repl])
|
85
|
+
end
|
86
|
+
|
87
|
+
alias_method :reset_rules, :load_rules
|
88
|
+
|
89
|
+
# Convenience method to retrieve the last review. Intended to be used from the REPL.
|
90
|
+
#
|
91
|
+
# @return [Review] The last review performed.
|
92
|
+
def review
|
93
|
+
@review
|
37
94
|
end
|
38
95
|
|
39
96
|
private
|
@@ -49,11 +106,6 @@ module FoodCritic
|
|
49
106
|
matches.respond_to?(:each) ? matches : []
|
50
107
|
end
|
51
108
|
|
52
|
-
# Load the rules from the (fairly unnecessary) DSL.
|
53
|
-
def load_rules
|
54
|
-
@rules = RuleDsl.load(File.join(File.dirname(__FILE__), 'rules.rb'))
|
55
|
-
end
|
56
|
-
|
57
109
|
# Return the files within a cookbook tree that we are interested in trying to match rules against.
|
58
110
|
#
|
59
111
|
# @param [String] dir The cookbook directory
|
@@ -64,5 +116,20 @@ module FoodCritic
|
|
64
116
|
Dir.glob(File.join(dir, '*/{attributes,providers,recipes}/*.rb'))
|
65
117
|
end
|
66
118
|
|
119
|
+
# Whether to fail the build.
|
120
|
+
#
|
121
|
+
# @param [Array] fail_tags The tags that should cause the build to fail, or special value 'any' for any tag.
|
122
|
+
# @param [Set] matched_tags The tags of warnings we have matches for
|
123
|
+
# @return [Boolean] True if the build should be failed
|
124
|
+
def should_fail_build?(fail_tags, matched_tags)
|
125
|
+
if fail_tags.empty?
|
126
|
+
false
|
127
|
+
elsif fail_tags.include? 'any'
|
128
|
+
true
|
129
|
+
else
|
130
|
+
Gherkin::TagExpression.new(fail_tags).eval(matched_tags)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
67
134
|
end
|
68
135
|
end
|
data/lib/foodcritic/rules.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
rule "FC001", "Use strings in preference to symbols to access node attributes" do
|
2
2
|
tags %w{style attributes}
|
3
|
-
description "When accessing node attributes you should use a string for a key rather than a symbol."
|
4
3
|
recipe do |ast|
|
5
4
|
%w{node default override set normal}.map do |type|
|
6
5
|
ast.xpath("//*[self::aref_field or self::aref][descendant::ident/@value='#{type}']//symbol").map{|ar| match(ar)}
|
@@ -10,7 +9,6 @@ end
|
|
10
9
|
|
11
10
|
rule "FC002", "Avoid string interpolation where not required" do
|
12
11
|
tags %w{style strings}
|
13
|
-
description "When setting a resource value avoid string interpolation where not required."
|
14
12
|
recipe do |ast|
|
15
13
|
ast.xpath(%q{//string_literal[count(descendant::string_embexpr) = 1 and
|
16
14
|
count(string_add/tstring_content|string_add/string_add/tstring_content) = 0]}).map{|str| match(str)}
|
@@ -19,15 +17,13 @@ end
|
|
19
17
|
|
20
18
|
rule "FC003", "Check whether you are running with chef server before using server-specific features" do
|
21
19
|
tags %w{portability solo}
|
22
|
-
|
23
|
-
|
24
|
-
checks_for_chef_solo?(ast) ? [] : searches(ast).map{|s| match(s)}
|
20
|
+
recipe do |ast,filename|
|
21
|
+
searches(ast).map{|s| match(s)} unless checks_for_chef_solo?(ast) or chef_solo_search_supported?(filename)
|
25
22
|
end
|
26
23
|
end
|
27
24
|
|
28
25
|
rule "FC004", "Use a service resource to start and stop services" do
|
29
26
|
tags %w{style services}
|
30
|
-
description "Avoid use of execute to control services - use the service resource instead."
|
31
27
|
recipe do |ast|
|
32
28
|
find_resources(ast, 'execute').find_all do |cmd|
|
33
29
|
cmd_str = (resource_attribute('command', cmd) || resource_name(cmd)).to_s
|
@@ -39,7 +35,6 @@ end
|
|
39
35
|
|
40
36
|
rule "FC005", "Avoid repetition of resource declarations" do
|
41
37
|
tags %w{style}
|
42
|
-
description "Where you have a lot of resources that vary in only a single attribute wrap them in a loop for brevity."
|
43
38
|
recipe do |ast|
|
44
39
|
matches = []
|
45
40
|
# do all of the attributes for all resources of a given type match apart aside from one?
|
@@ -55,7 +50,6 @@ end
|
|
55
50
|
|
56
51
|
rule "FC006", "Mode should be quoted or fully specified when setting file permissions" do
|
57
52
|
tags %w{correctness files}
|
58
|
-
description "Not quoting mode when setting permissions can lead to incorrect permissions being set."
|
59
53
|
recipe do |ast|
|
60
54
|
ast.xpath(%q{//ident[@value='mode']/parent::command/descendant::int[string-length(@value) < 4]/
|
61
55
|
ancestor::method_add_block}).map{|resource| match(resource)}
|
@@ -64,21 +58,19 @@ end
|
|
64
58
|
|
65
59
|
rule "FC007", "Ensure recipe dependencies are reflected in cookbook metadata" do
|
66
60
|
tags %w{correctness metadata}
|
67
|
-
description "You are including a recipe that is not in the current cookbook and not defined as a dependency in your cookbook metadata."
|
68
61
|
recipe do |ast,filename|
|
69
62
|
metadata_path = Pathname.new(File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
|
70
63
|
next unless File.exists? metadata_path
|
71
64
|
undeclared = included_recipes(ast).keys.map{|recipe|recipe.split('::').first} - [cookbook_name(filename)] -
|
72
65
|
declared_dependencies(read_file(metadata_path))
|
73
66
|
included_recipes(ast).map do |recipe, resource|
|
74
|
-
match(resource)
|
67
|
+
match(resource) if undeclared.include?(recipe) || undeclared.any?{|u| recipe.start_with?("#{u}::")}
|
75
68
|
end.compact
|
76
69
|
end
|
77
70
|
end
|
78
71
|
|
79
72
|
rule "FC008", "Generated cookbook metadata needs updating" do
|
80
73
|
tags %w{style metadata}
|
81
|
-
description "The cookbook metadata for this cookbook is boilerplate output from knife generate cookbook and needs updating with the real details of your cookbook."
|
82
74
|
cookbook do |filename|
|
83
75
|
metadata_path = Pathname.new(File.join(filename, 'metadata.rb')).cleanpath
|
84
76
|
next unless File.exists? metadata_path
|
@@ -93,7 +85,6 @@ end
|
|
93
85
|
|
94
86
|
rule "FC009", "Resource attribute not recognised" do
|
95
87
|
tags %w{correctness}
|
96
|
-
description "You appear to be using an unrecognised attribute on a standard Chef resource. Please check for typos."
|
97
88
|
recipe do |ast|
|
98
89
|
matches = []
|
99
90
|
resource_attributes_by_type(ast).each do |type,resources|
|
@@ -113,7 +104,6 @@ end
|
|
113
104
|
|
114
105
|
rule "FC010", "Invalid search syntax" do
|
115
106
|
tags %w{correctness search}
|
116
|
-
description "The search expression in the recipe could not be parsed. Please check your syntax."
|
117
107
|
recipe do |ast|
|
118
108
|
# This only works for literal search strings
|
119
109
|
literal_searches(ast).reject{|search| valid_query?(search['value'])}.map{|search| match(search)}
|
data/lib/foodcritic/version.rb
CHANGED
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.
|
4
|
+
version: 0.7.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: 2011-12-
|
12
|
+
date: 2011-12-31 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: chef
|
16
|
-
requirement: &
|
16
|
+
requirement: &2160561320 !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: *
|
24
|
+
version_requirements: *2160561320
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: json
|
27
|
-
requirement: &
|
27
|
+
requirement: &2160560580 !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: *
|
38
|
+
version_requirements: *2160560580
|
39
39
|
- !ruby/object:Gem::Dependency
|
40
40
|
name: gherkin
|
41
|
-
requirement: &
|
41
|
+
requirement: &2160559620 !ruby/object:Gem::Requirement
|
42
42
|
none: false
|
43
43
|
requirements:
|
44
44
|
- - ~>
|
@@ -46,10 +46,21 @@ dependencies:
|
|
46
46
|
version: 2.7.1
|
47
47
|
type: :runtime
|
48
48
|
prerelease: false
|
49
|
-
version_requirements: *
|
49
|
+
version_requirements: *2160559620
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: gist
|
52
|
+
requirement: &2160558760 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 2.0.4
|
58
|
+
type: :runtime
|
59
|
+
prerelease: false
|
60
|
+
version_requirements: *2160558760
|
50
61
|
- !ruby/object:Gem::Dependency
|
51
62
|
name: nokogiri
|
52
|
-
requirement: &
|
63
|
+
requirement: &2160557900 !ruby/object:Gem::Requirement
|
53
64
|
none: false
|
54
65
|
requirements:
|
55
66
|
- - ~>
|
@@ -57,7 +68,29 @@ dependencies:
|
|
57
68
|
version: 1.5.0
|
58
69
|
type: :runtime
|
59
70
|
prerelease: false
|
60
|
-
version_requirements: *
|
71
|
+
version_requirements: *2160557900
|
72
|
+
- !ruby/object:Gem::Dependency
|
73
|
+
name: pry
|
74
|
+
requirement: &2160556980 !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ~>
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 0.9.7.4
|
80
|
+
type: :runtime
|
81
|
+
prerelease: false
|
82
|
+
version_requirements: *2160556980
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry-doc
|
85
|
+
requirement: &2160556240 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ~>
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 0.3.0
|
91
|
+
type: :runtime
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: *2160556240
|
61
94
|
description: Lint tool for Opscode Chef cookbooks.
|
62
95
|
email:
|
63
96
|
executables:
|
@@ -94,12 +127,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
127
|
version: '0'
|
95
128
|
segments:
|
96
129
|
- 0
|
97
|
-
hash:
|
130
|
+
hash: -1380277540148398826
|
98
131
|
requirements: []
|
99
132
|
rubyforge_project:
|
100
133
|
rubygems_version: 1.8.10
|
101
134
|
signing_key:
|
102
135
|
specification_version: 3
|
103
|
-
summary: foodcritic-0.
|
136
|
+
summary: foodcritic-0.7.0
|
104
137
|
test_files: []
|
105
138
|
has_rdoc:
|