foodcritic 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)