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