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