foodcritic 0.11.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/foodcritic.rb CHANGED
@@ -9,7 +9,7 @@ require_relative 'foodcritic/chef'
9
9
  require_relative 'foodcritic/command_line'
10
10
  require_relative 'foodcritic/domain'
11
11
  require_relative 'foodcritic/error_checker'
12
- require_relative 'foodcritic/helpers'
12
+ require_relative 'foodcritic/api'
13
13
  require_relative 'foodcritic/dsl'
14
14
  require_relative 'foodcritic/linter'
15
15
  require_relative 'foodcritic/output'
@@ -3,36 +3,55 @@ require 'nokogiri'
3
3
  module FoodCritic
4
4
 
5
5
  # Helper methods that form part of the Rules DSL.
6
- module Helpers
6
+ module Api
7
7
 
8
8
  include FoodCritic::Chef
9
- include FoodCritic::Chef::Search
10
9
 
11
- # Create a match from the specified node.
10
+ # Find attribute accesses by type.
12
11
  #
13
- # @param [Nokogiri::XML::Node] node The node to create a match for
14
- # @return [Hash] Hash with the matched node name and position with the recipe
15
- def match(node)
16
- pos = node.xpath('descendant::pos').first
17
- {:matched => node.respond_to?(:name) ? node.name : '', :line => pos['line'], :column => pos['column']}
18
- end
12
+ # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check
13
+ # @param [Symbol] type The approach used to access the attributes
14
+ # (:string, :symbol or :vivified).
15
+ # @param [Boolean] ignore_calls Exclude attribute accesses that mix
16
+ # strings/symbols with dot notation. Defaults to false.
17
+ # @return [Array] The matching nodes if any
18
+ def attribute_access(ast, options = {})
19
+ options = {:type => :any, :ignore_calls => false}.merge!(options)
20
+ return [] unless ast.respond_to?(:xpath)
21
+ unless [:string, :symbol, :vivified].include?(options[:type])
22
+ raise ArgumentError, "Node type not recognised"
23
+ end
19
24
 
20
- # Create a match for a specified file. Use this if the presence of the file triggers the warning rather than content.
21
- #
22
- # @param [String] file The filename to create a match for
23
- # @return [Hash] Hash with the match details
24
- # @see FoodCritic::Helpers#match
25
- def file_match(file)
26
- {:filename => file, :matched => file, :line => 1, :column => 1}
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
34
+ 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)
41
+ end
42
+ ).sort
27
43
  end
28
44
 
29
45
  # Does the specified recipe check for Chef Solo?
30
46
  #
31
47
  # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
32
- # @return [Boolean] True if there is a test for Chef::Config[:solo] in the recipe
48
+ # @return [Boolean] True if there is a test for Chef::Config[:solo] in the
49
+ # recipe
33
50
  def checks_for_chef_solo?(ast)
34
- ! ast.xpath(%q{//if/aref[count(descendant::const[@value = 'Chef' or @value = 'Config']) = 2 and
35
- count(descendant::ident[@value='solo']) > 0]}).empty?
51
+ raise_unless_xpath!(ast)
52
+ ! ast.xpath(%q{//if/aref[count(descendant::const[@value = 'Chef' or
53
+ @value = 'Config']) = 2
54
+ and count(descendant::ident[@value='solo']) > 0]}).empty?
36
55
  end
37
56
 
38
57
  # Is the chef-solo-search library available?
@@ -40,93 +59,144 @@ module FoodCritic
40
59
  # @param [String] recipe_path The path to the current recipe
41
60
  # @return [Boolean] True if the chef-solo-search library is available.
42
61
  def chef_solo_search_supported?(recipe_path)
43
- search_libs = Dir[File.join(Pathname.new(File.join(recipe_path, '../../..')).realpath, "*/libraries/search.rb")]
62
+ return false if recipe_path.nil? || ! File.exists?(recipe_path)
63
+ cbk_tree_path = Pathname.new(File.join(recipe_path, '../../..'))
64
+ search_libs = Dir[File.join(cbk_tree_path.realpath, "*/libraries/search.rb")]
44
65
  search_libs.any? do |lib|
45
- ! read_file(lib).xpath(%q{//class[count(descendant::const[@value='Chef' or @value='Recipe']) = 2]/
46
- descendant::def/ident[@value='search']}).empty?
66
+ ! read_ast(lib).xpath(%q{//class[count(descendant::const[@value='Chef']
67
+ ) = 1]/descendant::def/ident[@value='search']}).empty?
47
68
  end
48
69
  end
49
70
 
50
- # Searches performed by the specified recipe.
71
+ # The name of the cookbook containing the specified file.
51
72
  #
52
- # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
53
- # @return [Boolean] True if the recipe performs a search
54
- def searches(ast)
55
- ast.xpath("//fcall/ident[@value = 'search']")
73
+ # @param [String] file The file in the cookbook
74
+ # @return [String] The name of the containing cookbook
75
+ def cookbook_name(file)
76
+ raise ArgumentError, 'File cannot be nil or empty' if file.to_s.empty?
77
+ until (file.split(File::SEPARATOR) & standard_cookbook_subdirs).empty? do
78
+ file = File.absolute_path(File.dirname(file.to_s))
79
+ end
80
+ file = File.dirname(file) unless File.extname(file).empty?
81
+ File.basename(file)
56
82
  end
57
83
 
58
- # Searches performed by the specified recipe that are literal strings. Searches with a query formed from a
59
- # subexpression will be ignored.
84
+ # The dependencies declared in cookbook metadata.
60
85
  #
61
- # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
62
- # @return [Nokogiri::XML::Node] The matching nodes
63
- def literal_searches(ast)
64
- ast.xpath("//method_add_arg[fcall/ident/@value = 'search' and count(descendant::string_embexpr) = 0]/descendant::tstring_content")
86
+ # @param [Nokogiri::XML::Node] ast The metadata rb AST
87
+ # @return [Array] List of cookbooks depended on
88
+ def declared_dependencies(ast)
89
+ raise_unless_xpath!(ast)
90
+ deps = ast.xpath(%q{//command[ident/@value='depends']/
91
+ descendant::args_add/descendant::tstring_content})
92
+ # handle quoted word arrays
93
+ var_ref = ast.xpath(%q{//command[ident/@value='depends']/
94
+ descendant::var_ref/ident})
95
+ unless var_ref.empty?
96
+ deps += ast.xpath(%Q{//block_var/params/ident#{var_ref.first['value']}/
97
+ ancestor::method_add_block/call/descendant::tstring_content})
98
+ end
99
+ deps.map{|dep| dep['value']}
100
+ end
101
+
102
+ # Create a match for a specified file. Use this if the presence of the file
103
+ # triggers the warning rather than content.
104
+ #
105
+ # @param [String] file The filename to create a match for
106
+ # @return [Hash] Hash with the match details
107
+ # @see FoodCritic::Api#match
108
+ def file_match(file)
109
+ raise ArgumentError, "Filename cannot be nil" if file.nil?
110
+ {:filename => file, :matched => file, :line => 1, :column => 1}
111
+ end
112
+
113
+ # Find Chef resources of the specified type.
114
+ # TODO: Include blockless resources
115
+ #
116
+ # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check
117
+ # @param [Hash] options The find options
118
+ # @option [Symbol] :type The type of resource to look for (or :any for all
119
+ # resources)
120
+ # @return [Array] AST nodes of Chef resources.
121
+ def find_resources(ast, options = {})
122
+ options = {:type => :any}.merge!(options)
123
+ return [] unless ast.respond_to?(:xpath)
124
+ scope_type = ''
125
+ scope_type = "[@value='#{options[:type]}']" unless options[:type] == :any
126
+ ast.xpath("//method_add_block[command/ident#{scope_type}]")
127
+ end
128
+
129
+ # Retrieve the recipes that are included within the given recipe AST.
130
+ #
131
+ # @param [Nokogiri::XML::Node] ast The recipe AST
132
+ # @return [Hash] include_recipe nodes keyed by included recipe name
133
+ def included_recipes(ast)
134
+ raise_unless_xpath!(ast)
135
+ # we only support literal strings, ignoring sub-expressions
136
+ included = ast.xpath(%q{//command[ident/@value = 'include_recipe' and
137
+ count(descendant::string_embexpr) = 0]/descendant::tstring_content})
138
+ included.inject(Hash.new([])){|h, i| h[i['value']] += [i]; h}
65
139
  end
66
140
 
67
141
  class AttFilter
68
142
  def is_att_type(value)
143
+ return [] unless value.respond_to?(:select)
69
144
  value.select{|n| %w{node default override set normal}.include?(n.to_s)}
70
145
  end
71
146
  end
72
147
 
73
- # Find attribute accesses by type.
148
+ # Searches performed by the specified recipe that are literal strings.
149
+ # Searches with a query formed from a subexpression will be ignored.
74
150
  #
75
151
  # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check
76
- # @param [Symbol] accessed_via The approach used to access the attributes (:string, :symbol or :vivified)
77
- # @param [Boolean] exclude_with_dots Exclude attribute accesses that mix strings/symbols with dot notation.
78
- # @return [Array] The matching nodes if any
79
- def attribute_access(ast, accessed_via, exclude_with_dots)
80
- (if accessed_via == :vivified
81
- calls = ast.xpath(%Q{//*[self::call or self::field][is_att_type(vcall/ident/@value) or
82
- is_att_type(var_ref/ident/@value)][@value='.']}, AttFilter.new)
83
- calls.select do |call|
84
- call.xpath("aref/args_add_block").size == 0 and (call.xpath("descendant::ident").size > 1 and
85
- ! dsl_methods.include?(call.xpath("ident/@value").to_s.to_sym))
86
- end
87
- else
88
- accessed_via = 'tstring_content' if accessed_via == :string
89
- expr = '//*[self::aref_field or self::aref][is_att_type(descendant::ident'
90
- expr += '[not(ancestor::aref/call)]' if exclude_with_dots
91
- expr += "/@value)]/descendant::#{accessed_via}"
92
- ast.xpath(expr, AttFilter.new)
93
- end
94
- ).sort
152
+ # @return [Array] The matching nodes
153
+ def literal_searches(ast)
154
+ return [] unless ast.respond_to?(:xpath)
155
+ ast.xpath("//method_add_arg[fcall/ident/@value = 'search' and
156
+ count(descendant::string_embexpr) = 0]/descendant::tstring_content")
95
157
  end
96
158
 
97
- # Find Chef resources of the specified type.
98
- # TODO: Include blockless resources
159
+ # Create a match from the specified node.
99
160
  #
100
- # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check
101
- # @param [String] type The type of resource to look for (or nil for all resources)
102
- # @return [Array] AST nodes of Chef resources.
103
- def find_resources(ast, type = nil)
104
- ast.xpath(%Q{//method_add_block[command/ident#{type.nil? ? '' : "[@value='#{type}']"}]})
161
+ # @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
163
+ def match(node)
164
+ raise_unless_xpath!(node)
165
+ pos = node.xpath('descendant::pos').first
166
+ return nil if pos.nil?
167
+ {:matched => node.respond_to?(:name) ? node.name : '',
168
+ :line => pos['line'].to_i, :column => pos['column'].to_i}
105
169
  end
106
170
 
107
- # Return the type, e.g. 'package' for a given resource
171
+ # Does the provided string look like an Operating System command? This is a
172
+ # rough heuristic to be taken with a pinch of salt.
108
173
  #
109
- # @param [Nokogiri::XML::Node] resource The resource AST
110
- # @return [String] The type of resource
111
- def resource_type(resource)
112
- resource.xpath('string(command/ident/@value)')
174
+ # @param [String] str The string to check
175
+ # @return [Boolean] True if this string might be an OS command
176
+ def os_command?(str)
177
+ str.start_with?('grep ', 'which ') or # common commands
178
+ str.include?('|') or # probably a pipe, could be alternation
179
+ str.match(/^[\w]+$/) or # command name only
180
+ str.match(/ --?[a-z]/i) # command-line flag
113
181
  end
114
182
 
115
- # Retrieve the name attribute associated with the specified resource.
183
+ # Read the AST for the given Ruby source file
116
184
  #
117
- # @param [Nokogiri::XML::Node] resource The resource AST to lookup the name attribute under
118
- # @return [String] The name attribute value
119
- def resource_name(resource)
120
- resource.xpath('string(command//tstring_content/@value)')
185
+ # @param [String] file The file to read
186
+ # @return [Nokogiri::XML::Node] The recipe AST
187
+ def read_ast(file)
188
+ build_xml(Ripper::SexpBuilder.new(File.read(file)).parse)
121
189
  end
122
190
 
123
191
  # Retrieve a single-valued attribute from the specified resource.
124
192
  #
193
+ # @param [Nokogiri::XML::Node] resource The resource AST to lookup the
194
+ # attribute under
125
195
  # @param [String] name The attribute name
126
- # @param [Nokogiri::XML::Node] resource The resource AST to lookup the attribute under
127
196
  # @return [String] The attribute value for the specified attribute
128
- def resource_attribute(name, resource)
129
- resource_attributes(resource)[name]
197
+ def resource_attribute(resource, name)
198
+ raise ArgumentError, "Attribute name cannot be empty" if name.empty?
199
+ resource_attributes(resource)[name.to_s]
130
200
  end
131
201
 
132
202
  # Retrieve all attributes from the specified resource.
@@ -134,8 +204,11 @@ module FoodCritic
134
204
  # @param [Nokogiri::XML::Node] resource The resource AST
135
205
  # @return [Hash] The resource attributes
136
206
  def resource_attributes(resource)
137
- atts = {:name => resource_name(resource)}
138
- resource.xpath('do_block/descendant::command[count(ancestor::do_block) = 1]').each do |att|
207
+ atts = {}
208
+ name = resource_name(resource)
209
+ atts[:name] = name unless name.empty?
210
+ resource.xpath('do_block/descendant::command
211
+ [count(ancestor::do_block) = 1]').each do |att|
139
212
  if att.xpath('descendant::symbol').empty?
140
213
  att_value = att.xpath('string(descendant::tstring_content/@value)')
141
214
  else
@@ -146,20 +219,10 @@ module FoodCritic
146
219
  atts
147
220
  end
148
221
 
149
- # Retrieve all resources of a given type
150
- #
151
- # @param [Nokogiri::XML::Node] ast The recipe AST
152
- # @return [Hash] The matching resources
153
- def resources_by_type(ast)
154
- result = Hash.new{|hash, key| hash[key] = Array.new}
155
- find_resources(ast).each{|resource| result[resource_type(resource)] << resource}
156
- result
157
- end
158
-
159
222
  # Retrieve the attributes as a hash for all resources of a given type.
160
223
  #
161
224
  # @param [Nokogiri::XML::Node] ast The recipe AST
162
- # @return [Hash] An array of resource attributes keyed by type.
225
+ # @return [Hash] Resources keyed by type, with an array for each
163
226
  def resource_attributes_by_type(ast)
164
227
  result = {}
165
228
  resources_by_type(ast).each do |type,resources|
@@ -168,47 +231,75 @@ module FoodCritic
168
231
  result
169
232
  end
170
233
 
171
- # Retrieve the recipes that are included within the given recipe AST.
234
+ # Retrieve the name attribute associated with the specified resource.
235
+ #
236
+ # @param [Nokogiri::XML::Node] resource The resource AST to lookup the name
237
+ # attribute under
238
+ # @return [String] The name attribute value
239
+ def resource_name(resource)
240
+ raise_unless_xpath!(resource)
241
+ resource.xpath('string(command//tstring_content/@value)')
242
+ end
243
+
244
+ # Retrieve all resources of a given type
172
245
  #
173
246
  # @param [Nokogiri::XML::Node] ast The recipe AST
174
- # @return [Hash] include_recipe nodes keyed by included recipe name
175
- def included_recipes(ast)
176
- # we only support literal strings, ignoring sub-expressions
177
- included = ast.xpath(%q{//command[ident/@value = 'include_recipe' and count(descendant::string_embexpr) = 0]/
178
- descendant::tstring_content})
179
- Hash[included.map{|recipe|recipe['value']}.zip(included)]
247
+ # @return [Hash] The matching resources
248
+ def resources_by_type(ast)
249
+ raise_unless_xpath!(ast)
250
+ result = Hash.new{|hash, key| hash[key] = Array.new}
251
+ find_resources(ast).each do |resource|
252
+ result[resource_type(resource)] << resource
253
+ end
254
+ result
180
255
  end
181
256
 
182
- # The name of the cookbook containing the specified file.
257
+ # Return the type, e.g. 'package' for a given resource
183
258
  #
184
- # @param [String] file The file in the cookbook
185
- # @return [String] The name of the containing cookbook
186
- def cookbook_name(file)
187
- File.basename(File.absolute_path(File.join(File.dirname(file), '..')))
259
+ # @param [Nokogiri::XML::Node] resource The resource AST
260
+ # @return [String] The type of resource
261
+ def resource_type(resource)
262
+ raise_unless_xpath!(resource)
263
+ type = resource.xpath('string(command/ident/@value)')
264
+ if type.empty?
265
+ raise ArgumentError, "Provided AST node is not a resource"
266
+ end
267
+ type
188
268
  end
189
269
 
190
- # The dependencies declared in cookbook metadata.
270
+ # Does the provided string look like ruby code?
191
271
  #
192
- # @param [Nokogiri::XML::Node] ast The metadata rb AST
193
- # @return [Array] List of cookbooks depended on
194
- def declared_dependencies(ast)
195
- deps = ast.xpath("//command[ident/@value='depends']/descendant::args_add/descendant::tstring_content")
196
- # handle quoted word arrays
197
- var_ref = ast.xpath("//command[ident/@value='depends']/descendant::var_ref/ident")
198
- deps += ast.xpath(%Q{//block_var/params/ident#{var_ref.first['value']}/ancestor::method_add_block/
199
- call/descendant::tstring_content}) unless var_ref.empty?
200
- deps.map{|dep| dep['value']}
272
+ # @param [String] str The string to check for rubiness
273
+ # @return [Boolean] True if this string could be syntactically valid Ruby
274
+ def ruby_code?(str)
275
+ str = str.to_s
276
+ return false if str.empty?
277
+ checker = FoodCritic::ErrorChecker.new(str)
278
+ checker.parse
279
+ ! checker.error?
201
280
  end
202
281
 
203
- # If the provided node is the line / column information.
282
+ # Searches performed by the specified recipe.
204
283
  #
205
- # @param [Nokogiri::XML::Node] node A node within the AST
206
- # @return [Boolean] True if this node holds the position data
207
- def position_node?(node)
208
- node.respond_to?(:length) and node.length == 2 and node.respond_to?(:all?) and node.all?{|child| child.respond_to?(:to_i)}
284
+ # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
285
+ # @return [Array] The AST nodes in the recipe where searches are performed
286
+ def searches(ast)
287
+ return [] unless ast.respond_to?(:xpath)
288
+ ast.xpath("//fcall/ident[@value = 'search']")
209
289
  end
210
290
 
211
- # Recurse the nested arrays provided by Ripper to create a tree we can more easily apply expressions to.
291
+ # The list of standard cookbook sub-directories.
292
+ #
293
+ # @return [Array] The standard list of directories.
294
+ def standard_cookbook_subdirs
295
+ %w{attributes definitions files libraries providers recipes resources
296
+ templates}
297
+ end
298
+
299
+ private
300
+
301
+ # Recurse the nested arrays provided by Ripper to create a tree we can more
302
+ # easily apply expressions to.
212
303
  #
213
304
  # @param [Array] node The AST
214
305
  # @param [Nokogiri::XML::Document] doc The document being constructed
@@ -228,7 +319,8 @@ module FoodCritic
228
319
  xml_node.add_child(pos)
229
320
  else
230
321
  if child.respond_to?(:first)
231
- n = Nokogiri::XML::Node.new(child.first.to_s.gsub(/[^a-z_]/, ''), doc)
322
+ n = Nokogiri::XML::Node.new(
323
+ child.first.to_s.gsub(/[^a-z_]/, ''), doc)
232
324
  xml_node.add_child(build_xml(child, doc, n))
233
325
  else
234
326
  xml_node['value'] = child.to_s unless child.nil?
@@ -239,34 +331,19 @@ module FoodCritic
239
331
  xml_node
240
332
  end
241
333
 
242
- # Read the AST for the given Ruby file
243
- #
244
- # @param [String] file The file to read
245
- # @return [Nokogiri::XML::Node] The recipe AST
246
- def read_file(file)
247
- build_xml(Ripper::SexpBuilder.new(IO.read(file)).parse)
248
- end
249
-
250
- # Does the provided string look like ruby code?
334
+ # If the provided node is the line / column information.
251
335
  #
252
- # @param [String] str The string to check for rubiness
253
- # @return [Boolean] True if this string could be syntactically valid Ruby
254
- def ruby_code?(str)
255
- checker = FoodCritic::ErrorChecker.new(str)
256
- checker.parse
257
- ! checker.error?
336
+ # @param [Nokogiri::XML::Node] node A node within the AST
337
+ # @return [Boolean] True if this node holds the position data
338
+ def position_node?(node)
339
+ node.respond_to?(:length) and node.length == 2 and
340
+ node.respond_to?(:all?) and node.all?{|child| child.respond_to?(:to_i)}
258
341
  end
259
342
 
260
- # Does the provided string look like an Operating System command? This is a rough heuristic to be taken with a
261
- # pinch of salt.
262
- #
263
- # @param [String] str The string to check
264
- # @return [Boolean] True if this string might be an OS command
265
- def os_command?(str)
266
- str.start_with?('grep ', 'which ') or # common commands
267
- str.include?('|') or # probably a pipe, could be alternation
268
- str.match(/^[\w]+$/) or # command name only
269
- str.match(/ --?[a-z]/i) # command-line flag
343
+ def raise_unless_xpath!(ast)
344
+ unless ast.respond_to?(:xpath)
345
+ raise ArgumentError, "AST must support #xpath"
346
+ end
270
347
  end
271
348
 
272
349
  end
@@ -3,31 +3,50 @@ module FoodCritic
3
3
  # Encapsulates functions that previously were calls to the Chef gem.
4
4
  module Chef
5
5
 
6
- def load_metadata
7
- metadata_path = File.join(File.dirname(__FILE__), '..', '..', 'chef_dsl_metadata.json')
8
- @dsl_metadata ||= Yajl::Parser.parse(IO.read(metadata_path), :symbolize_keys => true)
9
- end
10
-
11
6
  # The set of methods in the Chef DSL
12
7
  #
13
8
  # @return [Array] Array of method symbols
14
- def dsl_methods
9
+ def chef_dsl_methods
15
10
  load_metadata
16
11
  @dsl_metadata[:dsl_methods].map{|m| m.to_sym}
17
12
  end
18
13
 
19
- # Is the specified attribute valid for the type of resource?
14
+ # Is the specified attribute valid for the type of resource? Note that this
15
+ # method will return true if the resource_type is not recognised.
20
16
  #
21
17
  # @param [Symbol] resource_type The type of Chef resource
22
18
  # @param [Symbol] attribute_name The attribute name
23
- def attribute?(resource_type, attribute_name)
19
+ def resource_attribute?(resource_type, attribute_name)
20
+ if resource_type.to_s.empty? || attribute_name.to_s.empty?
21
+ raise ArgumentError, "Arguments cannot be nil or empty."
22
+ end
24
23
  load_metadata
25
24
  resource_attributes = @dsl_metadata[:attributes]
26
- return true unless resource_attributes.include?(resource_type)
27
- resource_attributes[resource_type].include?(attribute_name.to_s)
25
+ return true unless resource_attributes.include?(resource_type.to_sym)
26
+ resource_attributes[resource_type.to_sym].include?(attribute_name.to_s)
27
+ end
28
+
29
+ # Is this a valid Lucene query?
30
+ #
31
+ # @param [String] query The query to check for syntax errors
32
+ # @return [Boolean] True if the query is well-formed
33
+ def valid_query?(query)
34
+ raise ArgumentError, "Query cannot be nil or empty" if query.to_s.empty?
35
+ search = FoodCritic::Chef::Search.new
36
+ search.create_parser(search.chef_search_grammars)
37
+ search.parser? ? (! search.parser.parse(query.to_s).nil?) : true
28
38
  end
29
39
 
30
- module Search
40
+ private
41
+
42
+ def load_metadata
43
+ metadata_path = File.join(File.dirname(__FILE__), '..', '..',
44
+ 'chef_dsl_metadata.json')
45
+ @dsl_metadata ||= Yajl::Parser.parse(IO.read(metadata_path),
46
+ :symbolize_keys => true)
47
+ end
48
+
49
+ class Search
31
50
 
32
51
  # The search grammars that ship with any Chef gems installed locally.
33
52
  # These are returned in descending version order (a newer Chef version
@@ -43,7 +62,7 @@ module FoodCritic
43
62
  # Create the search parser from the first loadable grammar.
44
63
  #
45
64
  # @param [Array] grammar_paths Full paths to candidate treetop grammars
46
- def load_search_parser(grammar_paths)
65
+ def create_parser(grammar_paths)
47
66
  @search_parser ||= grammar_paths.inject(nil) do |parser,lucene_grammar|
48
67
  begin
49
68
  break parser unless parser.nil?
@@ -59,18 +78,14 @@ module FoodCritic
59
78
  # Has the search parser been loaded?
60
79
  #
61
80
  # @return [Boolean] True if the search parser has been loaded.
62
- def search_parser_loaded?
81
+ def parser?
63
82
  ! @search_parser.nil?
64
83
  end
65
84
 
66
- # Is this a valid Lucene query?
67
- #
68
- # @param [String] query The query to check for syntax errors
69
- # @return [Boolean] True if the query is well-formed
70
- def valid_query?(query)
71
- load_search_parser(chef_search_grammars)
72
- search_parser_loaded? ? (! @search_parser.parse(query).nil?) : true
85
+ def parser
86
+ @search_parser
73
87
  end
88
+
74
89
  end
75
90
  end
76
91
 
@@ -3,22 +3,23 @@ module FoodCritic
3
3
  # Command line parsing.
4
4
  class CommandLine
5
5
 
6
- include FoodCritic::Chef::Search
7
-
8
6
  # Create a new instance of CommandLine
9
7
  #
10
8
  # @param [Array] args The command line arguments
11
9
  def initialize(args)
12
10
  @args = args
11
+ @original_args = args.dup
13
12
  @options = {}
14
- @options[:fail_tags] = []; @options[:tags] = []
13
+ @options[:fail_tags] = []; @options[:tags] = []; @options[:include_rules] = []
15
14
  @parser = OptionParser.new do |opts|
16
15
  opts.banner = 'foodcritic [cookbook_path]'
17
16
  opts.on("-r", "--[no-]repl", "Drop into a REPL for interactive rule editing.") {|r|options[:repl] = r}
18
17
  opts.on("-t", "--tags TAGS", "Only check against rules with the specified tags.") {|t|options[:tags] << t}
19
18
  opts.on("-f", "--epic-fail TAGS", "Fail the build if any of the specified tags are matched.") {|t|options[:fail_tags] << t}
20
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
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}
22
23
  end
23
24
  @parser.parse!(args) unless show_help?
24
25
  end
@@ -37,6 +38,20 @@ module FoodCritic
37
38
  @parser.help
38
39
  end
39
40
 
41
+ # Show the current version to the end user?
42
+ #
43
+ # @return [Boolean] True if the version should be shown.
44
+ def show_version?
45
+ @options.key?(:version) and @original_args != ['-v']
46
+ end
47
+
48
+ # The version string.
49
+ #
50
+ # @return [String] Current installed version.
51
+ def version
52
+ "foodcritic #{FoodCritic::VERSION}"
53
+ end
54
+
40
55
  # If the cookbook path provided is valid
41
56
  #
42
57
  # @return [Boolean] True if the path is a directory that exists.
@@ -57,8 +72,9 @@ module FoodCritic
57
72
  def valid_grammar?
58
73
  return true unless options.key?(:search_grammar)
59
74
  return false unless File.exists?(options[:search_grammar])
60
- load_search_parser([options[:search_grammar]])
61
- search_parser_loaded?
75
+ search = FoodCritic::Chef::Search.new
76
+ search.create_parser([options[:search_grammar]])
77
+ search.parser?
62
78
  end
63
79
 
64
80
  # If matches should be shown with context rather than the default summary display.
@@ -51,7 +51,8 @@ module FoodCritic
51
51
 
52
52
  # A rule to be matched against.
53
53
  class Rule
54
- attr_accessor :code, :name, :cookbook, :recipe, :provider, :resource, :tags
54
+ attr_accessor :code, :name, :cookbook, :recipe, :provider, :resource
55
+ attr_writer :tags
55
56
 
56
57
  # Create a new rule
57
58
  #
@@ -62,6 +63,10 @@ module FoodCritic
62
63
  @tags = [code]
63
64
  end
64
65
 
66
+ def tags
67
+ ['any'] + @tags
68
+ end
69
+
65
70
  # Returns a string representation of this rule.
66
71
  #
67
72
  # @return [String] Rule as a string.
@@ -70,4 +75,4 @@ module FoodCritic
70
75
  end
71
76
  end
72
77
 
73
- end
78
+ end
@@ -7,12 +7,13 @@ module FoodCritic
7
7
  class RuleDsl
8
8
  attr_reader :rules
9
9
 
10
- include Helpers
10
+ include Api
11
11
 
12
12
  # Define a new rule
13
13
  #
14
14
  # @param [String] code The short unique identifier for this rule, e.g. 'FC001'
15
- # @param [String] name The short descriptive name of this rule presented to the end user.
15
+ # @param [String] name The short descriptive name of this rule presented to
16
+ # the end user.
16
17
  # @param [Block] block The rule definition
17
18
  def rule(code, name, &block)
18
19
  @rules = [] if @rules.nil?
@@ -57,14 +58,19 @@ module FoodCritic
57
58
 
58
59
  # Load the ruleset
59
60
  #
60
- # @param [String] filename The path to the ruleset to load
61
- # @return [Array] The loaded rules, ready to be matched against provided cookbooks.
62
- def self.load(filename, with_repl)
61
+ # @param [Array] paths The paths to the rulesets to load
62
+ # @return [Array] The loaded rules, ready to be matched against provided
63
+ # cookbooks.
64
+ def self.load(paths, with_repl)
63
65
  dsl = RuleDsl.new
64
- dsl.instance_eval(File.read(filename), filename)
66
+ paths.map do |path|
67
+ File.directory?(path) ? Dir["#{path}/**/*.rb"].sort : path
68
+ end.flatten.each do |path|
69
+ dsl.instance_eval(File.read(path), path)
70
+ end
65
71
  dsl.instance_eval { binding.pry } if with_repl
66
72
  dsl.rules
67
73
  end
68
74
  end
69
75
 
70
- end
76
+ end
@@ -8,18 +8,22 @@ module FoodCritic
8
8
  # The main entry point for linting your Chef cookbooks.
9
9
  class Linter
10
10
 
11
- include FoodCritic::Helpers
11
+ include FoodCritic::Api
12
12
 
13
- # Perform option parsing from the provided arguments and do a lint check based on those arguments.
13
+ # Perform option parsing from the provided arguments and do a lint check
14
+ # based on those arguments.
14
15
  #
15
16
  # @param [Array] args The command-line arguments to parse
16
- # @return [Array] Pair - the first item is string output, the second is the exit code.
17
+ # @return [Array] Pair - the first item is string output, the second is the
18
+ # exit code.
17
19
  def self.check(cmd_line)
18
20
  return [cmd_line.help, 0] if cmd_line.show_help?
21
+ return [cmd_line.version, 0] if cmd_line.show_version?
19
22
  if ! cmd_line.valid_grammar?
20
23
  [cmd_line.help, 4]
21
24
  elsif cmd_line.valid_path?
22
- review = FoodCritic::Linter.new.check(cmd_line.cookbook_path, cmd_line.options)
25
+ review = FoodCritic::Linter.new.check(cmd_line.cookbook_path,
26
+ cmd_line.options)
23
27
  [review, review.failed? ? 3 : 0]
24
28
  else
25
29
  [cmd_line.help, 2]
@@ -31,22 +35,27 @@ module FoodCritic
31
35
 
32
36
  end
33
37
 
34
- # Review the cookbooks at the provided path, identifying potential improvements.
38
+ # Review the cookbooks at the provided path, identifying potential
39
+ # improvements.
35
40
  #
36
- # @param [String] cookbook_path The file path to an individual cookbook directory
41
+ # @param [String] cookbook_path The file path to an individual cookbook
42
+ # directory
37
43
  # @param [Hash] options Options to apply to the linting
38
44
  # @option options [Array] tags The tags to filter rules based on
39
45
  # @option options [Array] fail_tags The tags to fail the build on
40
- # @return [FoodCritic::Review] A review of your cookbooks, with any warnings issued.
46
+ # @return [FoodCritic::Review] A review of your cookbooks, with any
47
+ # warnings issued.
41
48
  def check(cookbook_path, options)
49
+ raise ArgumentError, "Cookbook path is required" if cookbook_path.nil?
42
50
  @last_cookbook_path, @last_options = cookbook_path, options
43
51
  load_rules unless defined? @rules
44
52
  warnings = []; last_dir = nil; matched_rule_tags = Set.new
45
53
 
46
- active_rules = @rules.select{|rule| matching_tags?(options[:tags], rule.tags)}
54
+ active_rules = @rules.select{|rule| matching_tags?(options[:tags],
55
+ rule.tags)}
47
56
  files_to_process(cookbook_path).each do |file|
48
57
  cookbook_dir = Pathname.new(File.join(File.dirname(file), '..')).cleanpath
49
- ast = read_file(file)
58
+ ast = read_ast(file)
50
59
  active_rules.each do |rule|
51
60
  rule_matches = matches(rule.recipe, ast, file)
52
61
  rule_matches += matches(rule.provider, ast, file) if File.basename(File.dirname(file)) == 'providers'
@@ -54,13 +63,14 @@ module FoodCritic
54
63
  rule_matches += matches(rule.cookbook, cookbook_dir) if last_dir != cookbook_dir
55
64
  rule_matches.each do |match|
56
65
  warnings << Warning.new(rule, {:filename => file}.merge(match))
57
- matched_rule_tags += rule.tags
66
+ matched_rule_tags << rule.tags
58
67
  end
59
68
  end
60
69
  last_dir = cookbook_dir
61
70
  end
62
71
 
63
- @review = Review.new(cookbook_path, warnings, should_fail_build?(options[:fail_tags], matched_rule_tags))
72
+ @review = Review.new(cookbook_path, warnings,
73
+ should_fail_build?(options[:fail_tags], matched_rule_tags))
64
74
 
65
75
  binding.pry if options[:repl]
66
76
  @review
@@ -73,12 +83,14 @@ module FoodCritic
73
83
 
74
84
  # Load the rules from the (fairly unnecessary) DSL.
75
85
  def load_rules
76
- @rules = RuleDsl.load(File.join(File.dirname(__FILE__), 'rules.rb'), @last_options[:repl])
86
+ @rules = RuleDsl.load([File.join(File.dirname(__FILE__), 'rules.rb')] +
87
+ @last_options[:include_rules], @last_options[:repl])
77
88
  end
78
89
 
79
90
  alias_method :reset_rules, :load_rules
80
91
 
81
- # Convenience method to retrieve the last review. Intended to be used from the REPL.
92
+ # Convenience method to retrieve the last review. Intended to be used from
93
+ # the REPL.
82
94
  #
83
95
  # @return [Review] The last review performed.
84
96
  def review
@@ -98,29 +110,27 @@ module FoodCritic
98
110
  matches.respond_to?(:each) ? matches : []
99
111
  end
100
112
 
101
- # Return the files within a cookbook tree that we are interested in trying to match rules against.
113
+ # Return the files within a cookbook tree that we are interested in trying
114
+ # to match rules against.
102
115
  #
103
116
  # @param [String] dir The cookbook directory
104
- # @return [Array] The files underneath the provided directory to be processed.
117
+ # @return [Array] The files underneath the provided directory to be
118
+ # processed.
105
119
  def files_to_process(dir)
106
120
  return [dir] unless File.directory? dir
107
121
  Dir.glob(File.join(dir, '{attributes,providers,recipes,resources}/*.rb')) +
108
- Dir.glob(File.join(dir, '*/{attributes,providers,recipes,resources}/*.rb'))
122
+ Dir.glob(File.join(dir, '*/{attributes,providers,recipes,resources}/*.rb'))
109
123
  end
110
124
 
111
125
  # Whether to fail the build.
112
126
  #
113
- # @param [Array] fail_tags The tags that should cause the build to fail, or special value 'any' for any tag.
127
+ # @param [Array] fail_tags The tags that should cause the build to fail, or
128
+ # special value 'any' for any tag.
114
129
  # @param [Set] matched_tags The tags of warnings we have matches for
115
130
  # @return [Boolean] True if the build should be failed
116
131
  def should_fail_build?(fail_tags, matched_tags)
117
- if fail_tags.empty?
118
- false
119
- elsif fail_tags.include? 'any'
120
- true
121
- else
122
- matching_tags?(fail_tags, matched_tags)
123
- end
132
+ return false if fail_tags.empty?
133
+ matched_tags.any?{|tags| matching_tags?(fail_tags, tags)}
124
134
  end
125
135
 
126
136
  # Evaluate the specified tags
@@ -129,7 +139,9 @@ module FoodCritic
129
139
  # @param [Array] tags Tags to match against
130
140
  # @return [Boolean] True if the tags match
131
141
  def matching_tags?(tag_expr, tags)
132
- Gherkin::TagExpression.new(tag_expr).eval(tags.map{|t| Gherkin::Formatter::Model::Tag.new(t, 1)})
142
+ Gherkin::TagExpression.new(tag_expr).eval(tags.map do |t|
143
+ Gherkin::Formatter::Model::Tag.new(t, 1)
144
+ end)
133
145
  end
134
146
 
135
147
  end
@@ -1,7 +1,7 @@
1
1
  rule "FC001", "Use strings in preference to symbols to access node attributes" do
2
2
  tags %w{style attributes}
3
3
  recipe do |ast|
4
- attribute_access(ast, :symbol, false).map{|ar| match(ar)}
4
+ attribute_access(ast, :type => :symbol).map{|ar| match(ar)}
5
5
  end
6
6
  end
7
7
 
@@ -23,8 +23,8 @@ end
23
23
  rule "FC004", "Use a service resource to start and stop services" do
24
24
  tags %w{style services}
25
25
  recipe do |ast|
26
- find_resources(ast, 'execute').find_all do |cmd|
27
- cmd_str = (resource_attribute('command', cmd) || resource_name(cmd)).to_s
26
+ find_resources(ast, :type => 'execute').find_all do |cmd|
27
+ cmd_str = (resource_attribute(cmd, 'command') || resource_name(cmd)).to_s
28
28
  cmd_str.include?('/etc/init.d') || cmd_str.start_with?('service ') || cmd_str.start_with?('/sbin/service ') ||
29
29
  cmd_str.start_with?('start ') || cmd_str.start_with?('stop ') || cmd_str.start_with?('invoke-rc.d ')
30
30
  end.map{|cmd| match(cmd)}
@@ -58,11 +58,16 @@ rule "FC007", "Ensure recipe dependencies are reflected in cookbook metadata" do
58
58
  recipe do |ast,filename|
59
59
  metadata_path = Pathname.new(File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
60
60
  next unless File.exists? metadata_path
61
- undeclared = included_recipes(ast).keys.map{|recipe|recipe.split('::').first} - [cookbook_name(filename)] -
62
- declared_dependencies(read_file(metadata_path))
63
- included_recipes(ast).map do |recipe, resource|
64
- match(resource) if undeclared.include?(recipe) || undeclared.any?{|u| recipe.start_with?("#{u}::")}
65
- end.compact
61
+ undeclared = included_recipes(ast).keys.map do |recipe|
62
+ recipe.split('::').first
63
+ end - [cookbook_name(filename)] -
64
+ declared_dependencies(read_ast(metadata_path))
65
+ included_recipes(ast).map do |recipe, include_stmts|
66
+ if undeclared.include?(recipe) ||
67
+ undeclared.any?{|u| recipe.start_with?("#{u}::")}
68
+ include_stmts.map{|i| match(i)}
69
+ end
70
+ end.flatten.compact
66
71
  end
67
72
  end
68
73
 
@@ -71,7 +76,7 @@ rule "FC008", "Generated cookbook metadata needs updating" do
71
76
  cookbook do |filename|
72
77
  metadata_path = Pathname.new(File.join(filename, 'metadata.rb')).cleanpath
73
78
  next unless File.exists? metadata_path
74
- md = read_file(metadata_path)
79
+ md = read_ast(metadata_path)
75
80
  {'maintainer' => 'YOUR_COMPANY_NAME', 'maintainer_email' => 'YOUR_EMAIL'}.map do |field,value|
76
81
  md.xpath(%Q{//command[ident/@value='#{field}']/descendant::tstring_content[@value='#{value}']}).map do |m|
77
82
  match(m).merge(:filename => metadata_path)
@@ -86,8 +91,12 @@ rule "FC009", "Resource attribute not recognised" do
86
91
  matches = []
87
92
  resource_attributes_by_type(ast).each do |type,resources|
88
93
  resources.each do |resource|
89
- resource.keys.map{|att|att.to_sym}.reject{|att| attribute?(type.to_sym, att)}.each do |invalid_att|
90
- matches << match(find_resources(ast, type).find{|res|resource_attributes(res).include?(invalid_att.to_s)})
94
+ resource.keys.map{|att|att.to_sym}.reject do |att|
95
+ resource_attribute?(type.to_sym, att)
96
+ end.each do |invalid_att|
97
+ matches << match(find_resources(ast, :type => type).find do |res|
98
+ resource_attributes(res).include?(invalid_att.to_s)
99
+ end)
91
100
  end
92
101
  end
93
102
  end
@@ -120,8 +129,8 @@ end
120
129
  rule "FC013", "Use file_cache_path rather than hard-coding tmp paths" do
121
130
  tags %w{style files}
122
131
  recipe do |ast|
123
- find_resources(ast, 'remote_file').find_all do |download|
124
- path = (resource_attribute('path', download) || resource_name(download)).to_s
132
+ find_resources(ast, :type => 'remote_file').find_all do |download|
133
+ path = (resource_attribute(download, 'path') || resource_name(download)).to_s
125
134
  path.start_with?('/tmp/')
126
135
  end.map{|download| match(download)}
127
136
  end
@@ -130,7 +139,7 @@ end
130
139
  rule "FC014", "Consider extracting long ruby_block to library" do
131
140
  tags %w{style libraries}
132
141
  recipe do |ast|
133
- find_resources(ast, 'ruby_block').find_all do |rb|
142
+ find_resources(ast, :type => 'ruby_block').find_all do |rb|
134
143
  ! rb.xpath("//fcall[ident/@value='block' and count(ancestor::*) = 8]/../../do_block[count(descendant::*) > 100]").empty?
135
144
  end.map{|block| match(block)}
136
145
  end
@@ -172,9 +181,9 @@ end
172
181
  rule "FC019", "Access node attributes in a consistent manner" do
173
182
  tags %w{style attributes}
174
183
  cookbook do |cookbook_dir|
175
- asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].map{|file| {:path => file, :ast => read_file(file)}}
184
+ asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].map{|file| {:path => file, :ast => read_ast(file)}}
176
185
  types = [:string, :symbol, :vivified].map{|type| {:access_type => type, :count => files.map do |file|
177
- attribute_access(file[:ast], type, true).tap{|ast|
186
+ attribute_access(file[:ast], :type => type, :ignore_calls => true).tap{|ast|
178
187
  asts[type] = {:ast => ast, :path => file[:path]} if (! ast.empty?) and (! asts.has_key?(type))
179
188
  }.size
180
189
  end.inject(:+)}}.reject{|type| type[:count] == 0}
@@ -1,4 +1,4 @@
1
1
  module FoodCritic
2
2
  # The current version of foodcritic
3
- VERSION = '0.11.1'
3
+ VERSION = '1.0.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.11.1
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,25 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-29 00:00:00.000000000 Z
12
+ date: 2012-03-04 00:00:00.000000000 Z
13
13
  dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: json
16
- requirement: &2152841480 !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ! '>='
20
- - !ruby/object:Gem::Version
21
- version: 1.4.4
22
- - - <=
23
- - !ruby/object:Gem::Version
24
- version: 1.6.1
25
- type: :runtime
26
- prerelease: false
27
- version_requirements: *2152841480
28
14
  - !ruby/object:Gem::Dependency
29
15
  name: gherkin
30
- requirement: &2152869780 !ruby/object:Gem::Requirement
16
+ requirement: &2157955580 !ruby/object:Gem::Requirement
31
17
  none: false
32
18
  requirements:
33
19
  - - ~>
@@ -35,10 +21,10 @@ dependencies:
35
21
  version: 2.8.0
36
22
  type: :runtime
37
23
  prerelease: false
38
- version_requirements: *2152869780
24
+ version_requirements: *2157955580
39
25
  - !ruby/object:Gem::Dependency
40
26
  name: gist
41
- requirement: &2152867120 !ruby/object:Gem::Requirement
27
+ requirement: &2157954980 !ruby/object:Gem::Requirement
42
28
  none: false
43
29
  requirements:
44
30
  - - ~>
@@ -46,10 +32,10 @@ dependencies:
46
32
  version: 2.0.4
47
33
  type: :runtime
48
34
  prerelease: false
49
- version_requirements: *2152867120
35
+ version_requirements: *2157954980
50
36
  - !ruby/object:Gem::Dependency
51
37
  name: nokogiri
52
- requirement: &2152882420 !ruby/object:Gem::Requirement
38
+ requirement: &2157954500 !ruby/object:Gem::Requirement
53
39
  none: false
54
40
  requirements:
55
41
  - - ~>
@@ -57,10 +43,10 @@ dependencies:
57
43
  version: 1.5.0
58
44
  type: :runtime
59
45
  prerelease: false
60
- version_requirements: *2152882420
46
+ version_requirements: *2157954500
61
47
  - !ruby/object:Gem::Dependency
62
48
  name: pry
63
- requirement: &2152880280 !ruby/object:Gem::Requirement
49
+ requirement: &2157953920 !ruby/object:Gem::Requirement
64
50
  none: false
65
51
  requirements:
66
52
  - - ~>
@@ -68,10 +54,10 @@ dependencies:
68
54
  version: 0.9.7.4
69
55
  type: :runtime
70
56
  prerelease: false
71
- version_requirements: *2152880280
57
+ version_requirements: *2157953920
72
58
  - !ruby/object:Gem::Dependency
73
59
  name: pry-doc
74
- requirement: &2152877780 !ruby/object:Gem::Requirement
60
+ requirement: &2157953460 !ruby/object:Gem::Requirement
75
61
  none: false
76
62
  requirements:
77
63
  - - ~>
@@ -79,10 +65,10 @@ dependencies:
79
65
  version: 0.3.0
80
66
  type: :runtime
81
67
  prerelease: false
82
- version_requirements: *2152877780
68
+ version_requirements: *2157953460
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: rak
85
- requirement: &2152876160 !ruby/object:Gem::Requirement
71
+ requirement: &2157952960 !ruby/object:Gem::Requirement
86
72
  none: false
87
73
  requirements:
88
74
  - - ~>
@@ -90,10 +76,10 @@ dependencies:
90
76
  version: '1.4'
91
77
  type: :runtime
92
78
  prerelease: false
93
- version_requirements: *2152876160
79
+ version_requirements: *2157952960
94
80
  - !ruby/object:Gem::Dependency
95
81
  name: treetop
96
- requirement: &2152891220 !ruby/object:Gem::Requirement
82
+ requirement: &2157952180 !ruby/object:Gem::Requirement
97
83
  none: false
98
84
  requirements:
99
85
  - - ~>
@@ -101,10 +87,10 @@ dependencies:
101
87
  version: 1.4.10
102
88
  type: :runtime
103
89
  prerelease: false
104
- version_requirements: *2152891220
90
+ version_requirements: *2157952180
105
91
  - !ruby/object:Gem::Dependency
106
92
  name: yajl-ruby
107
- requirement: &2152888700 !ruby/object:Gem::Requirement
93
+ requirement: &2157951540 !ruby/object:Gem::Requirement
108
94
  none: false
109
95
  requirements:
110
96
  - - ~>
@@ -112,7 +98,7 @@ dependencies:
112
98
  version: 1.1.0
113
99
  type: :runtime
114
100
  prerelease: false
115
- version_requirements: *2152888700
101
+ version_requirements: *2157951540
116
102
  description: Lint tool for Opscode Chef cookbooks.
117
103
  email:
118
104
  executables:
@@ -120,12 +106,12 @@ executables:
120
106
  extensions: []
121
107
  extra_rdoc_files: []
122
108
  files:
109
+ - lib/foodcritic/api.rb
123
110
  - lib/foodcritic/chef.rb
124
111
  - lib/foodcritic/command_line.rb
125
112
  - lib/foodcritic/domain.rb
126
113
  - lib/foodcritic/dsl.rb
127
114
  - lib/foodcritic/error_checker.rb
128
- - lib/foodcritic/helpers.rb
129
115
  - lib/foodcritic/linter.rb
130
116
  - lib/foodcritic/output.rb
131
117
  - lib/foodcritic/rules.rb
@@ -154,12 +140,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
154
140
  version: '0'
155
141
  segments:
156
142
  - 0
157
- hash: -3754378815808582034
143
+ hash: 4610293132888604111
158
144
  requirements: []
159
145
  rubyforge_project:
160
146
  rubygems_version: 1.8.10
161
147
  signing_key:
162
148
  specification_version: 3
163
- summary: foodcritic-0.11.1
149
+ summary: foodcritic-1.0.0
164
150
  test_files: []
165
151
  has_rdoc: