foodcritic 1.4.0 → 1.5.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.
@@ -157,6 +157,325 @@
157
157
  "value_for_platform_family",
158
158
  "with_indexer_metadata"
159
159
  ],
160
+ "actions": {
161
+ "apt_package": [
162
+ "install",
163
+ "nothing",
164
+ "purge",
165
+ "reconfig",
166
+ "remove",
167
+ "upgrade"
168
+ ],
169
+ "bash": [
170
+ "nothing",
171
+ "run"
172
+ ],
173
+ "breakpoint": [
174
+ "break",
175
+ "nothing"
176
+ ],
177
+ "chef_gem": [
178
+ "install",
179
+ "nothing",
180
+ "purge",
181
+ "reconfig",
182
+ "remove",
183
+ "upgrade"
184
+ ],
185
+ "cookbook_file": [
186
+ "create",
187
+ "create_if_missing",
188
+ "delete",
189
+ "nothing",
190
+ "touch"
191
+ ],
192
+ "cron": [
193
+ "create",
194
+ "delete",
195
+ "nothing"
196
+ ],
197
+ "csh": [
198
+ "nothing",
199
+ "run"
200
+ ],
201
+ "deploy": [
202
+ "deploy",
203
+ "force_deploy",
204
+ "nothing",
205
+ "rollback"
206
+ ],
207
+ "deploy_branch": [
208
+ "deploy",
209
+ "force_deploy",
210
+ "nothing",
211
+ "rollback"
212
+ ],
213
+ "deploy_revision": [
214
+ "deploy",
215
+ "force_deploy",
216
+ "nothing",
217
+ "rollback"
218
+ ],
219
+ "directory": [
220
+ "create",
221
+ "delete",
222
+ "nothing"
223
+ ],
224
+ "dpkg_package": [
225
+ "install",
226
+ "nothing",
227
+ "purge",
228
+ "reconfig",
229
+ "remove",
230
+ "upgrade"
231
+ ],
232
+ "easy_install_package": [
233
+ "install",
234
+ "nothing",
235
+ "purge",
236
+ "reconfig",
237
+ "remove",
238
+ "upgrade"
239
+ ],
240
+ "env": [
241
+ "create",
242
+ "delete",
243
+ "modify",
244
+ "nothing"
245
+ ],
246
+ "erl_call": [
247
+ "nothing",
248
+ "run"
249
+ ],
250
+ "execute": [
251
+ "nothing",
252
+ "run"
253
+ ],
254
+ "file": [
255
+ "create",
256
+ "create_if_missing",
257
+ "delete",
258
+ "nothing",
259
+ "touch"
260
+ ],
261
+ "freebsd_package": [
262
+ "install",
263
+ "nothing",
264
+ "purge",
265
+ "reconfig",
266
+ "remove",
267
+ "upgrade"
268
+ ],
269
+ "gem_package": [
270
+ "install",
271
+ "nothing",
272
+ "purge",
273
+ "reconfig",
274
+ "remove",
275
+ "upgrade"
276
+ ],
277
+ "git": [
278
+ "checkout",
279
+ "diff",
280
+ "export",
281
+ "log",
282
+ "nothing",
283
+ "sync"
284
+ ],
285
+ "group": [
286
+ "create",
287
+ "manage",
288
+ "modify",
289
+ "nothing",
290
+ "remove"
291
+ ],
292
+ "http_request": [
293
+ "delete",
294
+ "get",
295
+ "head",
296
+ "nothing",
297
+ "options",
298
+ "post",
299
+ "put"
300
+ ],
301
+ "ifconfig": [
302
+ "add",
303
+ "delete",
304
+ "disable",
305
+ "enable",
306
+ "nothing"
307
+ ],
308
+ "link": [
309
+ "create",
310
+ "delete",
311
+ "nothing"
312
+ ],
313
+ "log": [
314
+ "nothing"
315
+ ],
316
+ "macports_package": [
317
+ "install",
318
+ "nothing",
319
+ "purge",
320
+ "reconfig",
321
+ "remove",
322
+ "upgrade"
323
+ ],
324
+ "mdadm": [
325
+ "assemble",
326
+ "create",
327
+ "nothing",
328
+ "stop"
329
+ ],
330
+ "mount": [
331
+ "disable",
332
+ "enable",
333
+ "mount",
334
+ "nothing",
335
+ "remount",
336
+ "umount"
337
+ ],
338
+ "ohai": [
339
+ "nothing",
340
+ "reload"
341
+ ],
342
+ "package": [
343
+ "install",
344
+ "nothing",
345
+ "purge",
346
+ "reconfig",
347
+ "remove",
348
+ "upgrade"
349
+ ],
350
+ "pacman_package": [
351
+ "install",
352
+ "nothing",
353
+ "purge",
354
+ "reconfig",
355
+ "remove",
356
+ "upgrade"
357
+ ],
358
+ "perl": [
359
+ "nothing",
360
+ "run"
361
+ ],
362
+ "portage_package": [
363
+ "install",
364
+ "nothing",
365
+ "purge",
366
+ "reconfig",
367
+ "remove",
368
+ "upgrade"
369
+ ],
370
+ "python": [
371
+ "nothing",
372
+ "run"
373
+ ],
374
+ "remote_directory": [
375
+ "create",
376
+ "create",
377
+ "create_if_missing",
378
+ "delete",
379
+ "delete",
380
+ "nothing"
381
+ ],
382
+ "remote_file": [
383
+ "create",
384
+ "create_if_missing",
385
+ "delete",
386
+ "nothing",
387
+ "touch"
388
+ ],
389
+ "route": [
390
+ "add",
391
+ "delete",
392
+ "nothing"
393
+ ],
394
+ "rpm_package": [
395
+ "install",
396
+ "nothing",
397
+ "purge",
398
+ "reconfig",
399
+ "remove",
400
+ "upgrade"
401
+ ],
402
+ "ruby": [
403
+ "nothing",
404
+ "run"
405
+ ],
406
+ "ruby_block": [
407
+ "create",
408
+ "nothing"
409
+ ],
410
+ "scm": [
411
+ "checkout",
412
+ "diff",
413
+ "export",
414
+ "log",
415
+ "nothing",
416
+ "sync"
417
+ ],
418
+ "script": [
419
+ "nothing",
420
+ "run"
421
+ ],
422
+ "service": [
423
+ "disable",
424
+ "enable",
425
+ "nothing",
426
+ "reload",
427
+ "restart",
428
+ "start",
429
+ "stop"
430
+ ],
431
+ "smart_o_s_package": [
432
+ "install",
433
+ "nothing",
434
+ "purge",
435
+ "reconfig",
436
+ "remove",
437
+ "upgrade"
438
+ ],
439
+ "subversion": [
440
+ "checkout",
441
+ "diff",
442
+ "export",
443
+ "force_export",
444
+ "log",
445
+ "nothing",
446
+ "sync"
447
+ ],
448
+ "template": [
449
+ "create",
450
+ "create_if_missing",
451
+ "delete",
452
+ "nothing",
453
+ "touch"
454
+ ],
455
+ "timestamped_deploy": [
456
+ "deploy",
457
+ "force_deploy",
458
+ "nothing",
459
+ "rollback"
460
+ ],
461
+ "user": [
462
+ "create",
463
+ "lock",
464
+ "manage",
465
+ "modify",
466
+ "nothing",
467
+ "remove",
468
+ "unlock"
469
+ ],
470
+ "yum_package": [
471
+ "install",
472
+ "nothing",
473
+ "purge",
474
+ "reconfig",
475
+ "remove",
476
+ "upgrade"
477
+ ]
478
+ },
160
479
  "attributes": {
161
480
  "apt_package": [
162
481
  "!",
data/lib/foodcritic.rb CHANGED
@@ -3,14 +3,22 @@ require 'gherkin'
3
3
  require 'treetop'
4
4
  require 'pry'
5
5
  require 'rak'
6
+ require 'ripper'
6
7
  require 'yajl'
8
+ require 'erubis'
7
9
 
8
10
  require_relative 'foodcritic/chef'
9
11
  require_relative 'foodcritic/command_line'
10
12
  require_relative 'foodcritic/domain'
11
13
  require_relative 'foodcritic/error_checker'
14
+ require_relative 'foodcritic/notifications'
15
+ require_relative 'foodcritic/ast'
16
+ require_relative 'foodcritic/xml'
12
17
  require_relative 'foodcritic/api'
18
+ require_relative 'foodcritic/repl'
13
19
  require_relative 'foodcritic/dsl'
14
20
  require_relative 'foodcritic/linter'
15
21
  require_relative 'foodcritic/output'
22
+ require_relative 'foodcritic/rake_task'
23
+ require_relative 'foodcritic/template'
16
24
  require_relative 'foodcritic/version'
@@ -5,37 +5,38 @@ module FoodCritic
5
5
  # Helper methods that form part of the Rules DSL.
6
6
  module Api
7
7
 
8
+ include FoodCritic::AST
9
+ include FoodCritic::XML
10
+
8
11
  include FoodCritic::Chef
12
+ include FoodCritic::Notifications
9
13
 
10
- # Find attribute accesses by type.
11
- #
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
14
+ # Find attribute access by type.
18
15
  def attribute_access(ast, options = {})
19
16
  options = {:type => :any, :ignore_calls => false}.merge!(options)
20
17
  return [] unless ast.respond_to?(:xpath)
21
- unless [:string, :symbol, :vivified].include?(options[:type])
18
+
19
+ unless [:any, :string, :symbol, :vivified].include?(options[:type])
22
20
  raise ArgumentError, "Node type not recognised"
23
21
  end
24
22
 
25
- if options[:type] == :vivified
26
- vivified_attribute_access(ast, options[:cookbook_dir])
27
- else
28
- standard_attribute_access(ast, options)
23
+ case options[:type]
24
+ when :any then
25
+ vivified_attribute_access(ast, options[:cookbook_dir]) +
26
+ standard_attribute_access(ast, options)
27
+ when :vivified then
28
+ vivified_attribute_access(ast, options[:cookbook_dir])
29
+ else
30
+ standard_attribute_access(ast, options)
29
31
  end
30
32
  end
31
33
 
32
34
  # Does the specified recipe check for Chef Solo?
33
- #
34
- # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
35
- # @return [Boolean] True if there is a test for Chef::Config[:solo],
36
- # Chef::Config['solo'] in the recipe
37
35
  def checks_for_chef_solo?(ast)
38
36
  raise_unless_xpath!(ast)
37
+
38
+ # TODO: This expression is too loose, but also will fail to match other
39
+ # types of conditionals.
39
40
  ! ast.xpath(%q{//if/*[self::aref or self::call][count(descendant::const[@value = 'Chef' or
40
41
  @value = 'Config']) = 2
41
42
  and
@@ -45,15 +46,23 @@ module FoodCritic
45
46
  ]}).empty?
46
47
  end
47
48
 
48
- # Is the chef-solo-search library available?
49
- #
50
- # @param [String] recipe_path The path to the current recipe
51
- # @return [Boolean] True if the chef-solo-search library is available.
49
+ # Is the [chef-solo-search library](https://github.com/edelight/chef-solo-search)
50
+ # available?
52
51
  def chef_solo_search_supported?(recipe_path)
53
52
  return false if recipe_path.nil? || ! File.exists?(recipe_path)
53
+
54
+ # Look for the chef-solo-search library.
55
+ #
56
+ # TODO: This will not work if the cookbook that contains the library
57
+ # is not under the same `cookbook_path` as the cookbook being checked.
54
58
  cbk_tree_path = Pathname.new(File.join(recipe_path, '../../..'))
55
59
  search_libs = Dir[File.join(cbk_tree_path.realpath,
56
60
  '*/libraries/search.rb')]
61
+
62
+ # True if any of the candidate library files match the signature:
63
+ #
64
+ # class Chef
65
+ # def search
57
66
  search_libs.any? do |lib|
58
67
  ! read_ast(lib).xpath(%q{//class[count(descendant::const[@value='Chef']
59
68
  ) = 1]/descendant::def/ident[@value='search']}).empty?
@@ -61,15 +70,17 @@ module FoodCritic
61
70
  end
62
71
 
63
72
  # The name of the cookbook containing the specified file.
64
- #
65
- # @param [String] file The file in the cookbook
66
- # @return [String] The name of the containing cookbook
67
73
  def cookbook_name(file)
68
74
  raise ArgumentError, 'File cannot be nil or empty' if file.to_s.empty?
75
+
69
76
  until (file.split(File::SEPARATOR) & standard_cookbook_subdirs).empty? do
70
77
  file = File.absolute_path(File.dirname(file.to_s))
71
78
  end
72
79
  file = File.dirname(file) unless File.extname(file).empty?
80
+ # We now have the name of the directory that contains the cookbook.
81
+
82
+ # We also need to consult the metadata in case the cookbook name has been
83
+ # overridden there. This supports only string literals.
73
84
  md_path = File.join(file, 'metadata.rb')
74
85
  if File.exists?(md_path)
75
86
  name = read_ast(md_path).xpath("//stmts_add/
@@ -80,14 +91,20 @@ module FoodCritic
80
91
  end
81
92
 
82
93
  # The dependencies declared in cookbook metadata.
83
- #
84
- # @param [Nokogiri::XML::Node] ast The metadata rb AST
85
- # @return [Array] List of cookbooks depended on
86
94
  def declared_dependencies(ast)
87
95
  raise_unless_xpath!(ast)
96
+
97
+ # String literals.
98
+ #
99
+ # depends 'foo'
88
100
  deps = ast.xpath(%q{//command[ident/@value='depends']/
89
101
  descendant::args_add/descendant::tstring_content[1]})
90
- # handle quoted word arrays
102
+
103
+ # Quoted word arrays are also common.
104
+ #
105
+ # %w{foo bar baz}.each do |cbk|
106
+ # depends cbk
107
+ # end
91
108
  var_ref = ast.xpath(%q{//command[ident/@value='depends']/
92
109
  descendant::var_ref/ident})
93
110
  unless var_ref.empty?
@@ -99,10 +116,6 @@ module FoodCritic
99
116
 
100
117
  # Create a match for a specified file. Use this if the presence of the file
101
118
  # triggers the warning rather than content.
102
- #
103
- # @param [String] file The filename to create a match for
104
- # @return [Hash] Hash with the match details
105
- # @see FoodCritic::Api#match
106
119
  def file_match(file)
107
120
  raise ArgumentError, "Filename cannot be nil" if file.nil?
108
121
  {:filename => file, :matched => file, :line => 1, :column => 1}
@@ -111,64 +124,58 @@ module FoodCritic
111
124
  # Find Chef resources of the specified type.
112
125
  # TODO: Include blockless resources
113
126
  #
114
- # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check
115
- # @param [Hash] options The find options
116
- # @option [Symbol] :type The type of resource to look for (or :any for all
117
- # resources)
118
- # @return [Array] AST nodes of Chef resources.
127
+ # These are equivalent:
128
+ #
129
+ # find_resources(ast)
130
+ # find_resources(ast, :type => :any)
131
+ #
132
+ # Restrict to a specific type of resource:
133
+ #
134
+ # find_resources(ast, :type => :service)
135
+ #
119
136
  def find_resources(ast, options = {})
120
137
  options = {:type => :any}.merge!(options)
121
138
  return [] unless ast.respond_to?(:xpath)
122
139
  scope_type = ''
123
140
  scope_type = "[@value='#{options[:type]}']" unless options[:type] == :any
124
- # XXX: include nested resources (provider actions)
141
+
142
+ # TODO: Include nested resources (provider actions)
125
143
  no_actions = "[command/ident/@value != 'action']"
126
144
  ast.xpath("//method_add_block[command/ident#{scope_type}]#{no_actions}")
127
145
  end
128
146
 
129
147
  # Helper to return a comparable version for a string.
130
- #
131
- # @param [String] version The version
132
- # @return [Gem::Version] The comparable version
133
148
  def gem_version(version)
134
149
  Gem::Version.create(version)
135
150
  end
136
151
 
137
152
  # Retrieve the recipes that are included within the given recipe AST.
138
153
  #
139
- # @param [Nokogiri::XML::Node] ast The recipe AST
140
- # @param [Hash] options Options to filter recipes to include
141
- # @option [Symbol] :with_partial_names Include string literals for recipes
142
- # that have an embedded sub-expression.
143
- # @return [Hash] include_recipe nodes keyed by included recipe name
154
+ # These two usages are equivalent:
155
+ #
156
+ # included_recipes(ast)
157
+ # included_recipes(ast, :with_partial_names => true)
158
+ #
144
159
  def included_recipes(ast, options = {:with_partial_names => true})
145
160
  raise_unless_xpath!(ast)
146
161
 
147
162
  filter = ['[count(descendant::args_add) = 1]']
163
+
164
+ # If `:with_partial_names` is false then we won't include the string
165
+ # literal portions of any string that has an embedded expression.
148
166
  unless options[:with_partial_names]
149
167
  filter << '[count(descendant::string_embexpr) = 0]'
150
168
  end
151
169
 
152
- # we only support literal strings, ignoring sub-expressions
153
- included = ast.xpath(%Q{//command[ident/@value = 'include_recipe']
154
- #{filter.join}
170
+ included = ast.xpath(%Q{//command[ident/@value = 'include_recipe']#{filter.join}
155
171
  [descendant::args_add/string_literal]/descendant::tstring_content})
156
- included.inject(Hash.new([])){|h, i| h[i['value']] += [i]; h}
157
- end
158
172
 
159
- # XPath custom function
160
- class AttFilter
161
- def is_att_type(value)
162
- return [] unless value.respond_to?(:select)
163
- value.select{|n| %w{node default override set normal}.include?(n.to_s)}
164
- end
173
+ # Hash keyed by recipe name with matched nodes.
174
+ included.inject(Hash.new([])){|h, i| h[i['value']] += [i]; h}
165
175
  end
166
176
 
167
177
  # Searches performed by the specified recipe that are literal strings.
168
178
  # Searches with a query formed from a subexpression will be ignored.
169
- #
170
- # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check
171
- # @return [Array] The matching nodes
172
179
  def literal_searches(ast)
173
180
  return [] unless ast.respond_to?(:xpath)
174
181
  ast.xpath("//method_add_arg[fcall/ident/@value = 'search' and
@@ -176,10 +183,6 @@ module FoodCritic
176
183
  end
177
184
 
178
185
  # Create a match from the specified node.
179
- #
180
- # @param [Nokogiri::XML::Node] node The node to create a match for
181
- # @return [Hash] Hash with the matched node name and position with the
182
- # recipe
183
186
  def match(node)
184
187
  raise_unless_xpath!(node)
185
188
  pos = node.xpath('descendant::pos').first
@@ -188,66 +191,8 @@ module FoodCritic
188
191
  :line => pos['line'].to_i, :column => pos['column'].to_i}
189
192
  end
190
193
 
191
- # Decode resource notifications.
192
- #
193
- # @param [Nokogiri::XML::Node] ast The AST to check for notifications.
194
- # @return [Array] A flat array of notifications. The resource_name may be
195
- # a string or a Node if the resource name is an expression.
196
- def notifications(ast)
197
- return [] unless ast.respond_to?(:xpath)
198
- ast.xpath('descendant::command[ident/@value="notifies" or
199
- ident/@value="subscribes"]').map do |notifies|
200
-
201
- params = notifies.xpath('descendant::method_add_arg[fcall/ident/
202
- @value="resources"]/descendant::assoc_new')
203
- timing = notifies.xpath('args_add_block/args_add/symbol_literal[last()]/
204
- symbol/ident[1]/@value')
205
- if params.empty?
206
- target = notifies.xpath('args_add_block/args_add/
207
- descendant::tstring_content/@value').to_s
208
- match = target.match(/^([^\[]+)\[(.*)\]$/)
209
- next unless match
210
- resource_type, resource_name =
211
- match.captures.tap{|m| m[0] = m[0].to_sym}
212
- if notifies.xpath('descendant::string_embexpr').empty?
213
- next if resource_name.empty?
214
- else
215
- resource_name =
216
- notifies.xpath('args_add_block/args_add/string_literal')
217
- end
218
- else
219
- resource_type = params.xpath('symbol[1]/ident/@value').to_s.to_sym
220
- resource_name = params.xpath('string_add[1][count(../
221
- descendant::string_add) = 1]/tstring_content/@value').to_s
222
- resource_name = params if resource_name.empty?
223
- end
224
- {
225
- :type =>
226
- notifies.xpath('ident/@value[1]').to_s.to_sym,
227
- :resource_type => resource_type,
228
- :resource_name => resource_name,
229
- :style => params.empty? ? :new : :old,
230
- :action =>
231
- notifies.xpath('descendant::symbol[1]/ident/@value').to_s.to_sym,
232
- :timing =>
233
- if timing.empty?
234
- :delayed
235
- else
236
- case timing.first.to_s.to_sym
237
- when :immediately, :immediate then :immediate
238
- else timing.first.to_s.to_sym
239
- end
240
- end
241
- }
242
-
243
- end.compact
244
- end
245
-
246
194
  # Does the provided string look like an Operating System command? This is a
247
195
  # rough heuristic to be taken with a pinch of salt.
248
- #
249
- # @param [String] str The string to check
250
- # @return [Boolean] True if this string might be an OS command
251
196
  def os_command?(str)
252
197
  str.start_with?('grep ', 'net ', 'which ') or # common commands
253
198
  str.include?('|') or # a pipe, could be alternation
@@ -257,62 +202,33 @@ module FoodCritic
257
202
  end
258
203
 
259
204
  # Read the AST for the given Ruby source file
260
- #
261
- # @param [String] file The file to read
262
- # @return [Nokogiri::XML::Node] The recipe AST
263
205
  def read_ast(file)
264
- build_xml(Ripper::SexpBuilder.new(File.read(file)).parse)
206
+ source = if file.to_s.end_with? '.erb'
207
+ Template::ExpressionExtractor.new.extract(
208
+ File.read(file)).map{|e| e[:code]}.join(';')
209
+ else
210
+ File.read(file)
211
+ end
212
+ build_xml(Ripper::SexpBuilder.new(source).parse)
265
213
  end
266
214
 
267
215
  # Retrieve a single-valued attribute from the specified resource.
268
- #
269
- # @param [Nokogiri::XML::Node] resource The resource AST to lookup the
270
- # attribute under
271
- # @param [String] name The attribute name
272
- # @return [String] The attribute value for the specified attribute
273
216
  def resource_attribute(resource, name)
274
217
  raise ArgumentError, "Attribute name cannot be empty" if name.empty?
275
218
  resource_attributes(resource)[name.to_s]
276
219
  end
277
220
 
278
221
  # Retrieve all attributes from the specified resource.
279
- #
280
- # @param [Nokogiri::XML::Node] resource The resource AST
281
- # @return [Hash] The resource attributes
282
- def resource_attributes(resource)
222
+ def resource_attributes(resource, options={})
283
223
  atts = {}
284
- name = resource_name(resource)
224
+ name = resource_name(resource, options)
285
225
  atts[:name] = name unless name.empty?
286
- resource.xpath('do_block/descendant::command
287
- [count(ancestor::do_block) = 1]').each do |att|
288
- att_value =
289
- if ! att.xpath('args_add_block[count(descendant::args_add)>1]').empty?
290
- att.xpath('args_add_block').first
291
- elsif ! att.xpath('args_add_block/args_add/
292
- var_ref/kw[@value="true" or @value="false"]').empty?
293
- att.xpath('string(args_add_block/args_add/
294
- var_ref/kw/@value)') == 'true'
295
- elsif att.xpath('descendant::symbol').empty?
296
- att.xpath('string(descendant::tstring_content/@value)')
297
- else
298
- att.xpath('string(descendant::symbol/ident/@value)').to_sym
299
- end
300
- atts[att.xpath('string(ident/@value)')] = att_value
301
- end
302
- resource.xpath("do_block/descendant::method_add_block[
303
- count(ancestor::do_block) = 1][brace_block | do_block]").each do |batt|
304
- att_name = batt.xpath('string(method_add_arg/fcall/ident/@value)')
305
- if att_name and ! att_name.empty? and batt.children.length > 1
306
- atts[att_name] = batt.children[1]
307
- end
308
- end
226
+ atts.merge!(normal_attributes(resource, options))
227
+ atts.merge!(block_attributes(resource))
309
228
  atts
310
229
  end
311
230
 
312
- # Retrieve the attributes as a hash for all resources of a given type.
313
- #
314
- # @param [Nokogiri::XML::Node] ast The recipe AST
315
- # @return [Hash] Resources keyed by type, with an array for each
231
+ # Resources keyed by type, with an array of matching nodes for each.
316
232
  def resource_attributes_by_type(ast)
317
233
  result = {}
318
234
  resources_by_type(ast).each do |type,resources|
@@ -322,19 +238,25 @@ module FoodCritic
322
238
  end
323
239
 
324
240
  # Retrieve the name attribute associated with the specified resource.
325
- #
326
- # @param [Nokogiri::XML::Node] resource The resource AST to lookup the name
327
- # attribute under
328
- # @return [String] The name attribute value
329
- def resource_name(resource)
241
+ def resource_name(resource, options = {})
330
242
  raise_unless_xpath!(resource)
331
- resource.xpath('string(command//tstring_content/@value)')
243
+ options = {:return_expressions => false}.merge(options)
244
+ if options[:return_expressions]
245
+ name = resource.xpath('command/args_add_block')
246
+ if name.xpath('descendant::string_add').size == 1 and
247
+ name.xpath('descendant::string_literal').size == 1 and
248
+ name.xpath('descendant::*[self::call or self::string_embexpr]').empty?
249
+ name.xpath('descendant::tstring_content/@value').to_s
250
+ else
251
+ name
252
+ end
253
+ else
254
+ # Preserve existing behaviour
255
+ resource.xpath('string(command//tstring_content/@value)')
256
+ end
332
257
  end
333
258
 
334
- # Retrieve all resources of a given type
335
- #
336
- # @param [Nokogiri::XML::Node] ast The recipe AST
337
- # @return [Hash] The matching resources
259
+ # Resources in an AST, keyed by type.
338
260
  def resources_by_type(ast)
339
261
  raise_unless_xpath!(ast)
340
262
  result = Hash.new{|hash, key| hash[key] = Array.new}
@@ -345,9 +267,6 @@ module FoodCritic
345
267
  end
346
268
 
347
269
  # Return the type, e.g. 'package' for a given resource
348
- #
349
- # @param [Nokogiri::XML::Node] resource The resource AST
350
- # @return [String] The type of resource
351
270
  def resource_type(resource)
352
271
  raise_unless_xpath!(resource)
353
272
  type = resource.xpath('string(command/ident/@value)')
@@ -358,70 +277,79 @@ module FoodCritic
358
277
  end
359
278
 
360
279
  # Does the provided string look like ruby code?
361
- #
362
- # @param [String] str The string to check for rubiness
363
- # @return [Boolean] True if this string could be syntactically valid Ruby
364
280
  def ruby_code?(str)
365
281
  str = str.to_s
366
282
  return false if str.empty?
283
+
367
284
  checker = FoodCritic::ErrorChecker.new(str)
368
285
  checker.parse
369
286
  ! checker.error?
370
287
  end
371
288
 
372
- # Searches performed by the specified recipe.
373
- #
374
- # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
375
- # @return [Array] The AST nodes in the recipe where searches are performed
289
+ # Searches performed by the provided AST.
376
290
  def searches(ast)
377
291
  return [] unless ast.respond_to?(:xpath)
378
292
  ast.xpath("//fcall/ident[@value = 'search']")
379
293
  end
380
294
 
381
295
  # The list of standard cookbook sub-directories.
382
- #
383
- # @return [Array] The standard list of directories.
384
296
  def standard_cookbook_subdirs
385
297
  %w{attributes definitions files libraries providers recipes resources
386
298
  templates}
387
299
  end
388
300
 
301
+ # Template filename
302
+ def template_file(resource)
303
+ if resource['source']
304
+ resource['source']
305
+ elsif resource[:name]
306
+ if resource[:name].respond_to?(:xpath)
307
+ resource[:name]
308
+ else
309
+ "#{File.basename(resource[:name])}.erb"
310
+ end
311
+ end
312
+ end
313
+
314
+ # Templates in the current cookbook
315
+ def template_paths(recipe_path)
316
+ Dir[Pathname.new(recipe_path).dirname.dirname + 'templates' + '**/*.erb']
317
+ end
318
+
389
319
  private
390
320
 
321
+ def block_attributes(resource)
322
+ # The attribute value may alternatively be a block, such as the meta
323
+ # conditionals `not_if` and `only_if`.
324
+ atts = {}
325
+ resource.xpath("do_block/descendant::method_add_block[
326
+ count(ancestor::do_block) = 1][brace_block | do_block]").each do |batt|
327
+ att_name = batt.xpath('string(method_add_arg/fcall/ident/@value)')
328
+ if att_name and ! att_name.empty? and batt.children.length > 1
329
+ atts[att_name] = batt.children[1]
330
+ end
331
+ end
332
+ atts
333
+ end
334
+
391
335
  # Recurse the nested arrays provided by Ripper to create a tree we can more
392
336
  # easily apply expressions to.
393
- #
394
- # @param [Array] node The AST
395
- # @param [Nokogiri::XML::Document] doc The document being constructed
396
- # @param [Nokogiri::XML::Node] xml_node The current node
397
- # @return [Nokogiri::XML::Node] The XML representation
398
337
  def build_xml(node, doc = nil, xml_node=nil)
399
- if doc.nil?
400
- doc = Nokogiri::XML('<opt></opt>')
401
- xml_node = doc.root
402
- end
338
+ doc, xml_node = xml_document(doc, xml_node)
339
+
403
340
  if node.respond_to?(:each)
341
+ # First child is the node name
404
342
  node.drop(1).each do |child|
405
343
  if position_node?(child)
406
- pos = Nokogiri::XML::Node.new("pos", doc)
407
- pos['line'] = child.first.to_s
408
- pos['column'] = child[1].to_s
409
- xml_node.add_child(pos)
344
+ xml_position_node(doc, xml_node, child)
410
345
  else
411
- if child.respond_to?(:first)
412
- if child.first.respond_to?(:first) and
413
- child.first.first == :assoc_new
414
- child.each do |c|
415
- n = Nokogiri::XML::Node.new(
416
- c.first.to_s.gsub(/[^a-z_]/, ''), doc)
417
- c.drop(1).each do |a|
418
- xml_node.add_child(build_xml(a, doc, n))
419
- end
420
- end
346
+ if ast_node_has_children?(child)
347
+ # The AST structure is different for hashes so we have to treat
348
+ # them separately.
349
+ if ast_hash_node?(child)
350
+ xml_hash_node(doc, xml_node, child)
421
351
  else
422
- n = Nokogiri::XML::Node.new(
423
- child.first.to_s.gsub(/[^a-z_]/, ''), doc)
424
- xml_node.add_child(build_xml(child, doc, n))
352
+ xml_array_node(doc, xml_node, child)
425
353
  end
426
354
  else
427
355
  xml_node['value'] = child.to_s unless child.nil?
@@ -432,14 +360,58 @@ module FoodCritic
432
360
  xml_node
433
361
  end
434
362
 
363
+ def extract_attribute_value(att, options = {})
364
+ if ! att.xpath('args_add_block[count(descendant::args_add)>1]').empty?
365
+ att.xpath('args_add_block').first
366
+ elsif ! att.xpath('args_add_block/args_add/
367
+ var_ref/kw[@value="true" or @value="false"]').empty?
368
+ att.xpath('string(args_add_block/args_add/
369
+ var_ref/kw/@value)') == 'true'
370
+ elsif ! att.xpath('descendant::assoc_new').empty?
371
+ att.xpath('descendant::assoc_new')
372
+ elsif att.xpath('descendant::symbol').empty?
373
+ if options[:return_expressions] and
374
+ (att.xpath('descendant::string_add').size != 1 or
375
+ ! att.xpath('descendant::*[self::call or self::string_embexpr]').empty?)
376
+ att
377
+ else
378
+ att.xpath('string(descendant::tstring_content/@value)')
379
+ end
380
+ else
381
+ att.xpath('string(descendant::symbol/ident/@value)').to_sym
382
+ end
383
+ end
384
+
435
385
  def node_method?(meth, cookbook_dir)
436
386
  chef_dsl_methods.include?(meth) || patched_node_method?(meth, cookbook_dir)
437
387
  end
438
388
 
389
+ def normal_attributes(resource, options = {})
390
+ atts = {}
391
+ # The ancestor check here ensures that nested blocks are not returned.
392
+ # For example a method call within a `ruby_block` would otherwise be
393
+ # returned as an attribute.
394
+ #
395
+ # TODO: This may need to be revisted in light of recent changes to the
396
+ # application cookbook which is popularising nested blocks.
397
+ resource.xpath('do_block/descendant::*[self::command or
398
+ self::method_add_arg][count(ancestor::do_block) = 1]').each do |att|
399
+
400
+ unless att.xpath('string(ident/@value | fcall/ident/@value)').empty?
401
+ atts[att.xpath('string(ident/@value | fcall/ident/@value)')] =
402
+ extract_attribute_value(att, options)
403
+ end
404
+ end
405
+ atts
406
+ end
407
+
439
408
  def patched_node_method?(meth, cookbook_dir)
440
409
  return false if cookbook_dir.nil? || ! Dir.exists?(cookbook_dir)
410
+
411
+ # TODO: Modify this to work with multiple cookbook paths
441
412
  cbk_tree_path = Pathname.new(File.join(cookbook_dir, '..'))
442
413
  libs = Dir[File.join(cbk_tree_path.realpath, '*/libraries/*.rb')]
414
+
443
415
  libs.any? do |lib|
444
416
  ! read_ast(lib).xpath(%Q{//class[count(descendant::const[@value='Chef'])
445
417
  > 0][count(descendant::const[@value='Node']) > 0]/descendant::def/
@@ -447,29 +419,34 @@ module FoodCritic
447
419
  end
448
420
  end
449
421
 
450
- # If the provided node is the line / column information.
451
- #
452
- # @param [Nokogiri::XML::Node] node A node within the AST
453
- # @return [Boolean] True if this node holds the position data
454
- def position_node?(node)
455
- node.respond_to?(:length) and node.length == 2 and
456
- node.respond_to?(:all?) and node.all?{|child| child.respond_to?(:to_i)}
457
- end
458
-
459
422
  def raise_unless_xpath!(ast)
460
423
  unless ast.respond_to?(:xpath)
461
424
  raise ArgumentError, "AST must support #xpath"
462
425
  end
463
426
  end
464
427
 
428
+ # XPath custom function
429
+ class AttFilter
430
+ def is_att_type(value)
431
+ return [] unless value.respond_to?(:select)
432
+ value.select{|n| %w{node default override set normal}.include?(n.to_s)}
433
+ end
434
+ end
435
+
465
436
  def standard_attribute_access(ast, options)
466
- type = options[:type] == :string ? 'tstring_content' : options[:type]
467
- expr = '//*[self::aref_field or self::aref]'
468
- expr += '[is_att_type(descendant::ident'
469
- expr += '[not(ancestor::aref/call)]' if options[:ignore_calls]
470
- expr += "/@value)]/descendant::#{type}"
471
- expr += "[ident/@value != 'node']" if type == :symbol
472
- ast.xpath(expr, AttFilter.new).sort
437
+ if options[:type] == :any
438
+ [:string, :symbol].map do |type|
439
+ standard_attribute_access(ast, options.merge(:type => type))
440
+ end.inject(:+)
441
+ else
442
+ type = options[:type] == :string ? 'tstring_content' : options[:type]
443
+ expr = '//*[self::aref_field or self::aref]'
444
+ expr += '[is_att_type(descendant::ident'
445
+ expr += '[not(ancestor::aref/call)]' if options[:ignore_calls]
446
+ expr += "/@value)]/descendant::#{type}"
447
+ expr += "[ident/@value != 'node']" if type == :symbol
448
+ ast.xpath(expr, AttFilter.new).sort
449
+ end
473
450
  end
474
451
 
475
452
  def vivified_attribute_access(ast, cookbook_dir)