foodcritic 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/foodcritic/api.rb +75 -7
- data/lib/foodcritic/linter.rb +1 -1
- data/lib/foodcritic/rules.rb +79 -2
- data/lib/foodcritic/version.rb +1 -1
- metadata +4 -4
data/lib/foodcritic/api.rb
CHANGED
@@ -36,7 +36,7 @@ module FoodCritic
|
|
36
36
|
# Chef::Config['solo'] in the recipe
|
37
37
|
def checks_for_chef_solo?(ast)
|
38
38
|
raise_unless_xpath!(ast)
|
39
|
-
! ast.xpath(%q{//if
|
39
|
+
! ast.xpath(%q{//if/*[self::aref or self::call][count(descendant::const[@value = 'Chef' or
|
40
40
|
@value = 'Config']) = 2
|
41
41
|
and
|
42
42
|
( count(descendant::ident[@value='solo']) > 0
|
@@ -137,13 +137,22 @@ module FoodCritic
|
|
137
137
|
# Retrieve the recipes that are included within the given recipe AST.
|
138
138
|
#
|
139
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.
|
140
143
|
# @return [Hash] include_recipe nodes keyed by included recipe name
|
141
|
-
def included_recipes(ast)
|
144
|
+
def included_recipes(ast, options = {:with_partial_names => true})
|
142
145
|
raise_unless_xpath!(ast)
|
146
|
+
|
147
|
+
filter = ['[count(descendant::args_add) = 1]']
|
148
|
+
unless options[:with_partial_names]
|
149
|
+
filter << '[count(descendant::string_embexpr) = 0]'
|
150
|
+
end
|
151
|
+
|
143
152
|
# we only support literal strings, ignoring sub-expressions
|
144
|
-
included = ast.xpath(%
|
145
|
-
|
146
|
-
descendant::tstring_content})
|
153
|
+
included = ast.xpath(%Q{//command[ident/@value = 'include_recipe']
|
154
|
+
#{filter.join}
|
155
|
+
[descendant::args_add/string_literal]/descendant::tstring_content})
|
147
156
|
included.inject(Hash.new([])){|h, i| h[i['value']] += [i]; h}
|
148
157
|
end
|
149
158
|
|
@@ -179,6 +188,61 @@ module FoodCritic
|
|
179
188
|
:line => pos['line'].to_i, :column => pos['column'].to_i}
|
180
189
|
end
|
181
190
|
|
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
|
+
|
182
246
|
# Does the provided string look like an Operating System command? This is a
|
183
247
|
# rough heuristic to be taken with a pinch of salt.
|
184
248
|
#
|
@@ -224,6 +288,10 @@ module FoodCritic
|
|
224
288
|
att_value =
|
225
289
|
if ! att.xpath('args_add_block[count(descendant::args_add)>1]').empty?
|
226
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'
|
227
295
|
elsif att.xpath('descendant::symbol').empty?
|
228
296
|
att.xpath('string(descendant::tstring_content/@value)')
|
229
297
|
else
|
@@ -406,8 +474,8 @@ module FoodCritic
|
|
406
474
|
|
407
475
|
def vivified_attribute_access(ast, cookbook_dir)
|
408
476
|
calls = ast.xpath(%q{//*[self::call or self::field]
|
409
|
-
[is_att_type(vcall/ident/@value) or
|
410
|
-
|
477
|
+
[is_att_type(vcall/ident/@value) or is_att_type(var_ref/ident/@value)]
|
478
|
+
[@value='.'][count(following-sibling::arg_paren) = 0]}, AttFilter.new)
|
411
479
|
calls.select do |call|
|
412
480
|
call.xpath("aref/args_add_block").size == 0 and
|
413
481
|
(call.xpath("descendant::ident").size > 1 and
|
data/lib/foodcritic/linter.rb
CHANGED
@@ -78,7 +78,7 @@ module FoodCritic
|
|
78
78
|
rule_matches += matches(rule.provider, ast, file)
|
79
79
|
end
|
80
80
|
if File.basename(File.dirname(file)) == 'resources'
|
81
|
-
|
81
|
+
rule_matches += matches(rule.resource, ast, file)
|
82
82
|
end
|
83
83
|
if last_dir != cookbook_dir
|
84
84
|
rule_matches += matches(rule.cookbook, cookbook_dir)
|
data/lib/foodcritic/rules.rb
CHANGED
@@ -85,11 +85,12 @@ rule "FC007", "Ensure recipe dependencies are reflected in cookbook metadata" do
|
|
85
85
|
metadata_path =Pathname.new(
|
86
86
|
File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
|
87
87
|
next unless File.exists? metadata_path
|
88
|
-
|
88
|
+
actual_included = included_recipes(ast, :with_partial_names => false)
|
89
|
+
undeclared = actual_included.keys.map do |recipe|
|
89
90
|
recipe.split('::').first
|
90
91
|
end - [cookbook_name(filename)] -
|
91
92
|
declared_dependencies(read_ast(metadata_path))
|
92
|
-
|
93
|
+
actual_included.map do |recipe, include_stmts|
|
93
94
|
if undeclared.include?(recipe) ||
|
94
95
|
undeclared.any?{|u| recipe.start_with?("#{u}::")}
|
95
96
|
include_stmts
|
@@ -378,3 +379,79 @@ rule "FC026", "Conditional execution block attribute contains only string" do
|
|
378
379
|
end
|
379
380
|
end
|
380
381
|
end
|
382
|
+
|
383
|
+
rule "FC027", "Resource sets internal attribute" do
|
384
|
+
tags %w{correctness}
|
385
|
+
recipe do |ast|
|
386
|
+
find_resources(ast, :type => :service).map do |service|
|
387
|
+
service unless (resource_attributes(service).keys &
|
388
|
+
['enabled', 'running']).empty?
|
389
|
+
end.compact
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
rule "FC028", "Incorrect #platform? usage" do
|
394
|
+
tags %w{correctness}
|
395
|
+
recipe do |ast|
|
396
|
+
ast.xpath(%q{//*[self::call | self::command_call]
|
397
|
+
[(var_ref|vcall)/ident/@value='node']
|
398
|
+
[ident/@value="platform?"]})
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
rule "FC029", "No leading cookbook name in recipe metadata" do
|
403
|
+
tags %w{correctness metadata}
|
404
|
+
cookbook do |filename|
|
405
|
+
metadata_path = Pathname.new(File.join(filename, 'metadata.rb')).cleanpath
|
406
|
+
next unless File.exists? metadata_path
|
407
|
+
read_ast(metadata_path).xpath('//command[ident/@value="recipe"]').map do |declared_recipe|
|
408
|
+
next unless declared_recipe.xpath('count(//vcall|//var_ref)').to_i == 0
|
409
|
+
recipe_name = declared_recipe.xpath('args_add_block/
|
410
|
+
descendant::tstring_content[1]/@value').to_s
|
411
|
+
unless recipe_name.empty? ||
|
412
|
+
recipe_name.split('::').first == cookbook_name(filename.to_s)
|
413
|
+
declared_recipe
|
414
|
+
end
|
415
|
+
end.compact.map {|m| match(m).merge(:filename => metadata_path.to_s) }
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
rule "FC030", "Cookbook contains debugger breakpoints" do
|
420
|
+
tags %w{annoyances}
|
421
|
+
cookbook do |cookbook_dir|
|
422
|
+
Dir[cookbook_dir + '**/*.rb'].map do |ruby_file|
|
423
|
+
read_ast(ruby_file).xpath('//call[(vcall|var_ref)/ident/@value="binding"]
|
424
|
+
[ident/@value="pry"]').map do |bp|
|
425
|
+
match(bp).merge({:filename => ruby_file})
|
426
|
+
end
|
427
|
+
end +
|
428
|
+
Dir[cookbook_dir + 'templates/**/*.erb'].map do |template_file|
|
429
|
+
IO.read(template_file).lines.with_index(1).map do |line, line_number|
|
430
|
+
# Not properly parsing the template
|
431
|
+
if line =~ /binding\.pry/
|
432
|
+
{:filename => template_file, :line => line_number}
|
433
|
+
end
|
434
|
+
end.compact
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
rule "FC031", "Cookbook without metadata file" do
|
440
|
+
tags %w{correctness metadata}
|
441
|
+
cookbook do |filename|
|
442
|
+
if ! File.exists?(File.join(filename, 'metadata.rb'))
|
443
|
+
[file_match(File.join(filename, 'metadata.rb'))]
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
rule "FC032", "Invalid notification timing" do
|
449
|
+
tags %w{correctness notifications}
|
450
|
+
recipe do |ast|
|
451
|
+
find_resources(ast).select do |resource|
|
452
|
+
notifications(resource).any? do |notification|
|
453
|
+
! [:delayed, :immediate].include? notification[:timing]
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
data/lib/foodcritic/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foodcritic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-06-
|
12
|
+
date: 2012-06-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: gherkin
|
@@ -181,12 +181,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
181
181
|
version: '0'
|
182
182
|
segments:
|
183
183
|
- 0
|
184
|
-
hash:
|
184
|
+
hash: -3352294596244898334
|
185
185
|
requirements: []
|
186
186
|
rubyforge_project:
|
187
187
|
rubygems_version: 1.8.19
|
188
188
|
signing_key:
|
189
189
|
specification_version: 3
|
190
|
-
summary: foodcritic-1.
|
190
|
+
summary: foodcritic-1.4.0
|
191
191
|
test_files: []
|
192
192
|
has_rdoc:
|