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.
- data/chef_dsl_metadata.json +319 -0
- data/lib/foodcritic.rb +8 -0
- data/lib/foodcritic/api.rb +204 -227
- data/lib/foodcritic/ast.rb +22 -0
- data/lib/foodcritic/chef.rb +46 -31
- data/lib/foodcritic/command_line.rb +12 -2
- data/lib/foodcritic/domain.rb +15 -34
- data/lib/foodcritic/dsl.rb +42 -43
- data/lib/foodcritic/linter.rb +123 -120
- data/lib/foodcritic/notifications.rb +145 -0
- data/lib/foodcritic/rake_task.rb +34 -0
- data/lib/foodcritic/repl.rb +28 -0
- data/lib/foodcritic/rules.rb +84 -49
- data/lib/foodcritic/template.rb +44 -0
- data/lib/foodcritic/version.rb +1 -1
- data/lib/foodcritic/xml.rb +40 -0
- metadata +34 -12
data/chef_dsl_metadata.json
CHANGED
|
@@ -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'
|
data/lib/foodcritic/api.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
18
|
+
|
|
19
|
+
unless [:any, :string, :symbol, :vivified].include?(options[:type])
|
|
22
20
|
raise ArgumentError, "Node type not recognised"
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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)
|