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
@@ -0,0 +1,145 @@
|
|
1
|
+
module FoodCritic
|
2
|
+
|
3
|
+
# This module contains the logic for the parsing of
|
4
|
+
# [Chef Notifications](http://wiki.opscode.com/display/chef/Resources#Resources-Notifications).
|
5
|
+
module Notifications
|
6
|
+
|
7
|
+
# Extracts notification details from the provided AST, returning an
|
8
|
+
# array of notification hashes.
|
9
|
+
#
|
10
|
+
# template "/etc/www/configures-apache.conf" do
|
11
|
+
# notifies :restart, "service[apache]"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# => [{:resource_name=>"apache",
|
15
|
+
# :resource_type=>:service,
|
16
|
+
# :type=>:notifies,
|
17
|
+
# :style=>:new,
|
18
|
+
# :action=>:restart,
|
19
|
+
# :timing=>:delayed}]
|
20
|
+
#
|
21
|
+
def notifications(ast)
|
22
|
+
# Sanity check the AST provided.
|
23
|
+
return [] unless ast.respond_to?(:xpath)
|
24
|
+
|
25
|
+
# We are mapping each `notifies` or `subscribes` line in the provided
|
26
|
+
# AST to a Hash with the extracted details.
|
27
|
+
notification_nodes(ast).map do |notify|
|
28
|
+
|
29
|
+
# Chef supports two styles of notification.
|
30
|
+
notified_resource = if new_style_notification?(notify)
|
31
|
+
# `notifies :restart, "service[foo]"`
|
32
|
+
new_style_notification(notify)
|
33
|
+
else
|
34
|
+
# `notifies :restart, resources(:service => "foo")`
|
35
|
+
old_style_notification(notify)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Ignore if the notification was not parsed
|
39
|
+
next unless notified_resource
|
40
|
+
|
41
|
+
# Now merge the extract notification details with the attributes
|
42
|
+
# that are common to both styles of notification.
|
43
|
+
notified_resource.merge({
|
44
|
+
# The `:type` of notification: `:subscribes` or `:notifies`.
|
45
|
+
:type => notification_type(notify),
|
46
|
+
|
47
|
+
# The `:style` of notification: `:new` or `:old`.
|
48
|
+
:style => new_style_notification?(notify) ? :new : :old,
|
49
|
+
|
50
|
+
# The target resource action.
|
51
|
+
:action => notification_action(notify),
|
52
|
+
|
53
|
+
# The notification timing. Either `:immediate` or `:delayed`.
|
54
|
+
:timing => notification_timing(notify)
|
55
|
+
})
|
56
|
+
end.compact
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Extract the `:resource_name` and `:resource_type` from a new-style
|
62
|
+
# notification.
|
63
|
+
def new_style_notification(notify)
|
64
|
+
|
65
|
+
# Given `notifies :restart, "service[foo]"` the target is the
|
66
|
+
# `"service[foo]"` string portion.
|
67
|
+
target_path = 'args_add_block/args_add/descendant::tstring_content/@value'
|
68
|
+
target = notify.xpath("arg_paren/#{target_path} | #{target_path}").to_s
|
69
|
+
|
70
|
+
# Test the target string against the standard syntax for a new-style
|
71
|
+
# notification: `resource_type[resource_name]`.
|
72
|
+
match = target.match(/^([^\[]+)\[(.*)\]$/)
|
73
|
+
return nil unless match
|
74
|
+
|
75
|
+
# Convert the captured resource type and name to symbols.
|
76
|
+
resource_type, resource_name =
|
77
|
+
match.captures.tap{|m| m[0] = m[0].to_sym}
|
78
|
+
|
79
|
+
# Normally the `resource_name` will be a simple string. However in the
|
80
|
+
# case where it has an embedded sub-expression then we will return the
|
81
|
+
# AST to the caller to handle.
|
82
|
+
if notify.xpath('descendant::string_embexpr').empty?
|
83
|
+
return nil if resource_name.empty?
|
84
|
+
else
|
85
|
+
resource_name =
|
86
|
+
notify.xpath('args_add_block/args_add/string_literal')
|
87
|
+
end
|
88
|
+
{:resource_name => resource_name, :resource_type => resource_type}
|
89
|
+
end
|
90
|
+
|
91
|
+
# Extract the `:resource_name` and `:resource_type` from an old-style
|
92
|
+
# notification.
|
93
|
+
def old_style_notification(notify)
|
94
|
+
resources = resource_hash_references(notify)
|
95
|
+
resource_type = resources.xpath('symbol[1]/ident/@value').to_s.to_sym
|
96
|
+
resource_name = resources.xpath('string_add[1][count(../
|
97
|
+
descendant::string_add) = 1]/tstring_content/@value').to_s
|
98
|
+
resource_name = resources if resource_name.empty?
|
99
|
+
{:resource_name => resource_name, :resource_type => resource_type}
|
100
|
+
end
|
101
|
+
|
102
|
+
def notification_timing(notify)
|
103
|
+
# The notification timing should be the last symbol on the notifies element.
|
104
|
+
timing = notify.xpath('args_add_block/args_add/symbol_literal[last()]/
|
105
|
+
symbol/ident[1]/@value')
|
106
|
+
if timing.empty?
|
107
|
+
# "By default, notifications are :delayed"
|
108
|
+
:delayed
|
109
|
+
else
|
110
|
+
case timing.first.to_s.to_sym
|
111
|
+
# Both forms are valid, but we return `:immediate` for both to avoid
|
112
|
+
# the caller having to recognise both.
|
113
|
+
when :immediately, :immediate then :immediate
|
114
|
+
|
115
|
+
# Pass the timing through unmodified if we don't recognise it.
|
116
|
+
else timing.first.to_s.to_sym
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def new_style_notification?(notify)
|
122
|
+
resource_hash_references(notify).empty?
|
123
|
+
end
|
124
|
+
|
125
|
+
def notification_action(notify)
|
126
|
+
notify.xpath('descendant::symbol[1]/ident/@value').to_s.to_sym
|
127
|
+
end
|
128
|
+
|
129
|
+
def notification_nodes(ast, &block)
|
130
|
+
type_path = '[ident/@value="notifies" or ident/@value="subscribes"]'
|
131
|
+
ast.xpath("descendant::command#{type_path} |
|
132
|
+
descendant::method_add_arg[fcall#{type_path}]")
|
133
|
+
end
|
134
|
+
|
135
|
+
def notification_type(notify)
|
136
|
+
notify.xpath('ident/@value[1] | fcall/ident/@value[1]').to_s.to_sym
|
137
|
+
end
|
138
|
+
|
139
|
+
def resource_hash_references(ast)
|
140
|
+
ast.xpath('descendant::method_add_arg[fcall/ident/
|
141
|
+
@value="resources"]/descendant::assoc_new')
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
|
4
|
+
module FoodCritic
|
5
|
+
module Rake
|
6
|
+
class LintTask < ::Rake::TaskLib
|
7
|
+
attr_accessor :name, :files
|
8
|
+
attr_writer :options
|
9
|
+
|
10
|
+
def initialize(name = :foodcritic)
|
11
|
+
@name = name
|
12
|
+
@files = Dir.pwd
|
13
|
+
@options = {}
|
14
|
+
yield self if block_given?
|
15
|
+
define
|
16
|
+
end
|
17
|
+
|
18
|
+
def options
|
19
|
+
{:fail_tags => ['correctness'], # differs to default cmd-line behaviour
|
20
|
+
:exclude_paths => ['test/**/*']}.merge(@options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def define
|
24
|
+
desc "Lint Chef cookbooks"
|
25
|
+
task(name) do
|
26
|
+
result = FoodCritic::Linter.new.check(files, options)
|
27
|
+
puts result
|
28
|
+
fail if result.failed?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module FoodCritic
|
2
|
+
module REPL
|
3
|
+
|
4
|
+
# Convenience method to repeat the last check. Intended to be used from the
|
5
|
+
# REPL.
|
6
|
+
def recheck
|
7
|
+
check(@last_cookbook_paths, @last_options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def reset_rules
|
11
|
+
load_rules!(@last_options)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Convenience method to retrieve the last review. Intended to be used from
|
15
|
+
# the REPL.
|
16
|
+
def review
|
17
|
+
@review
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_repl(cookbook_paths, options)
|
21
|
+
@last_cookbook_paths, @last_options = cookbook_paths, options
|
22
|
+
@review = yield if block_given?
|
23
|
+
binding.pry if options[:repl]
|
24
|
+
@review
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/lib/foodcritic/rules.rb
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# This file contains all of the rules that ship with foodcritic.
|
2
|
+
#
|
3
|
+
# * Foodcritic rules perform static code analysis - rather than the cookbook code
|
4
|
+
# being loaded by the interpreter it is parsed into a tree (AST) that is then
|
5
|
+
# passed to each rule.
|
6
|
+
# * Rules can use a number of API functions that ship with foodcritic to make
|
7
|
+
# sense of the parse tree.
|
8
|
+
# * Rules can also use XPath to query the AST. A rule can consist of a XPath
|
9
|
+
# query only, as any nodes returned from a `recipe` block will be converted
|
10
|
+
# into warnings.
|
11
|
+
|
1
12
|
rule "FC001",
|
2
13
|
"Use strings in preference to symbols to access node attributes" do
|
3
14
|
tags %w{style attributes}
|
@@ -101,17 +112,12 @@ end
|
|
101
112
|
|
102
113
|
rule "FC008", "Generated cookbook metadata needs updating" do
|
103
114
|
tags %w{style metadata}
|
104
|
-
|
105
|
-
metadata_path = Pathname.new(File.join(filename, 'metadata.rb')).cleanpath
|
106
|
-
next unless File.exists? metadata_path
|
107
|
-
md = read_ast(metadata_path)
|
115
|
+
metadata do |ast,filename|
|
108
116
|
{'maintainer' => 'YOUR_COMPANY_NAME',
|
109
117
|
'maintainer_email' => 'YOUR_EMAIL'}.map do |field,value|
|
110
|
-
|
111
|
-
descendant::tstring_content[@value='#{value}']})
|
112
|
-
|
113
|
-
end
|
114
|
-
end.flatten
|
118
|
+
ast.xpath(%Q{//command[ident/@value='#{field}']/
|
119
|
+
descendant::tstring_content[@value='#{value}']})
|
120
|
+
end
|
115
121
|
end
|
116
122
|
end
|
117
123
|
|
@@ -253,26 +259,6 @@ rule "FC019", "Access node attributes in a consistent manner" do
|
|
253
259
|
end
|
254
260
|
end
|
255
261
|
|
256
|
-
rule "FC020", "Conditional execution string attribute looks like Ruby" do
|
257
|
-
tags %w{correctness}
|
258
|
-
applies_to {|version| version >= gem_version("0.7.4")}
|
259
|
-
recipe do |ast, filename|
|
260
|
-
conditions = ast.xpath(%q{//command[(ident/@value='only_if' or ident/
|
261
|
-
@value='not_if') and descendant::tstring_content]}).map{|m| match(m)}
|
262
|
-
unless conditions.empty?
|
263
|
-
lines = File.readlines(filename) # go back for the raw untokenized string
|
264
|
-
conditions.map do |condition|
|
265
|
-
line = lines[(condition[:line].to_i) -1]
|
266
|
-
{:match => condition,
|
267
|
-
:raw_string => line.strip.sub(/^(not|only)_if[\s+]["']/, '').chop}
|
268
|
-
end.find_all do |cond|
|
269
|
-
ruby_code?(cond[:raw_string]) and
|
270
|
-
! os_command?(cond[:raw_string])
|
271
|
-
end.map{|cond| cond[:match]}
|
272
|
-
end
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
262
|
rule "FC021", "Resource condition in provider may not behave as expected" do
|
277
263
|
tags %w{correctness lwrp}
|
278
264
|
applies_to {|version| version >= gem_version("0.10.6")}
|
@@ -401,10 +387,8 @@ end
|
|
401
387
|
|
402
388
|
rule "FC029", "No leading cookbook name in recipe metadata" do
|
403
389
|
tags %w{correctness metadata}
|
404
|
-
|
405
|
-
|
406
|
-
next unless File.exists? metadata_path
|
407
|
-
read_ast(metadata_path).xpath('//command[ident/@value="recipe"]').map do |declared_recipe|
|
390
|
+
metadata do |ast,filename|
|
391
|
+
ast.xpath('//command[ident/@value="recipe"]').map do |declared_recipe|
|
408
392
|
next unless declared_recipe.xpath('count(//vcall|//var_ref)').to_i == 0
|
409
393
|
recipe_name = declared_recipe.xpath('args_add_block/
|
410
394
|
descendant::tstring_content[1]/@value').to_s
|
@@ -412,28 +396,19 @@ rule "FC029", "No leading cookbook name in recipe metadata" do
|
|
412
396
|
recipe_name.split('::').first == cookbook_name(filename.to_s)
|
413
397
|
declared_recipe
|
414
398
|
end
|
415
|
-
end.compact
|
399
|
+
end.compact
|
416
400
|
end
|
417
401
|
end
|
418
402
|
|
419
403
|
rule "FC030", "Cookbook contains debugger breakpoints" do
|
420
404
|
tags %w{annoyances}
|
421
|
-
|
422
|
-
|
423
|
-
|
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
|
405
|
+
recipe do |ast|
|
406
|
+
ast.xpath('//call[(vcall|var_ref)/ident/@value="binding"]
|
407
|
+
[ident/@value="pry"]')
|
436
408
|
end
|
409
|
+
library{recipe}
|
410
|
+
metadata{recipe}
|
411
|
+
template{recipe}
|
437
412
|
end
|
438
413
|
|
439
414
|
rule "FC031", "Cookbook without metadata file" do
|
@@ -455,3 +430,63 @@ rule "FC032", "Invalid notification timing" do
|
|
455
430
|
end
|
456
431
|
end
|
457
432
|
end
|
433
|
+
|
434
|
+
rule "FC033", "Missing template" do
|
435
|
+
tags %w{correctness}
|
436
|
+
recipe do |ast,filename|
|
437
|
+
find_resources(ast, :type => :template).reject do |resource|
|
438
|
+
resource_attributes(resource)['local']
|
439
|
+
end.map do |resource|
|
440
|
+
file = template_file(resource_attributes(resource,
|
441
|
+
:return_expressions => true))
|
442
|
+
{:resource => resource, :file => file}
|
443
|
+
end.reject do |resource|
|
444
|
+
resource[:file].respond_to?(:xpath)
|
445
|
+
end.select do |resource|
|
446
|
+
template_paths(filename).none? do |path|
|
447
|
+
relative_path = []
|
448
|
+
Pathname.new(path).ascend do |template_path|
|
449
|
+
relative_path << template_path.basename
|
450
|
+
break if template_path.dirname.dirname.basename.to_s == 'templates'
|
451
|
+
end
|
452
|
+
File.join(relative_path.reverse) == resource[:file]
|
453
|
+
end
|
454
|
+
end.map{|resource| resource[:resource]}
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
rule "FC034", "Unused template variables" do
|
459
|
+
tags %w{correctness}
|
460
|
+
recipe do |ast,filename|
|
461
|
+
Array(resource_attributes_by_type(ast)['template']).select do
|
462
|
+
|t| t['variables'] and t['variables'].respond_to?(:xpath)
|
463
|
+
end.map do |resource|
|
464
|
+
template_paths = Dir[Pathname.new(filename).dirname.dirname +
|
465
|
+
'templates' + '**/*.erb']
|
466
|
+
template_path = template_paths.find{|p| File.basename(p) == resource['source']}
|
467
|
+
next unless template_path
|
468
|
+
passed_vars = resource['variables'].xpath('symbol/ident/@value').map{|tv| tv.to_s}
|
469
|
+
template_vars = read_ast(template_path).xpath('//var_ref/ivar/' +
|
470
|
+
'@value').map{|v| v.to_s.sub(/^@/, '')}
|
471
|
+
file_match(template_path) unless (passed_vars - template_vars).empty?
|
472
|
+
end.compact
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
rule "FC035", "Template uses node attribute directly" do
|
477
|
+
tags %w{style}
|
478
|
+
template do |ast,filename|
|
479
|
+
[file_match(filename)] unless attribute_access(ast).empty?
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
rule "FC037", "Invalid notification action" do
|
484
|
+
tags %w{correctness}
|
485
|
+
recipe do |ast|
|
486
|
+
find_resources(ast).select do |resource|
|
487
|
+
notifications(resource).any? do |n|
|
488
|
+
! resource_action?(n[:resource_type], n[:action])
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module FoodCritic
|
2
|
+
module Template
|
3
|
+
|
4
|
+
# Extract expressions <%= expr %> from Erb templates.
|
5
|
+
class ExpressionExtractor
|
6
|
+
|
7
|
+
include Erubis::Basic::Converter
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
init_converter({})
|
11
|
+
end
|
12
|
+
|
13
|
+
def extract(template_code)
|
14
|
+
@expressions = []
|
15
|
+
convert(template_code)
|
16
|
+
@expressions
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_expr(src, code, indicator)
|
20
|
+
if indicator == '='
|
21
|
+
@expressions << {:type => :expression, :code => code.strip}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_text(src, text)
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_preamble(codebuf)
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_postamble(codebuf)
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_stmt(src, code)
|
38
|
+
@expressions << {:type => :statement, :code => code.strip}
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
data/lib/foodcritic/version.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
module FoodCritic
|
2
|
+
module XML
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def xml_array_node(doc, xml_node, child)
|
7
|
+
n = xml_create_node(doc, child)
|
8
|
+
xml_node.add_child(build_xml(child, doc, n))
|
9
|
+
end
|
10
|
+
|
11
|
+
def xml_create_node(doc, c)
|
12
|
+
Nokogiri::XML::Node.new(c.first.to_s.gsub(/[^a-z_]/, ''), doc)
|
13
|
+
end
|
14
|
+
|
15
|
+
def xml_document(doc, xml_node)
|
16
|
+
if doc.nil?
|
17
|
+
doc = Nokogiri::XML('<opt></opt>')
|
18
|
+
xml_node = doc.root
|
19
|
+
end
|
20
|
+
[doc, xml_node]
|
21
|
+
end
|
22
|
+
|
23
|
+
def xml_hash_node(doc, xml_node, child)
|
24
|
+
child.each do |c|
|
25
|
+
n = xml_create_node(doc, c)
|
26
|
+
c.drop(1).each do |a|
|
27
|
+
xml_node.add_child(build_xml(a, doc, n))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def xml_position_node(doc, xml_node, child)
|
33
|
+
pos = Nokogiri::XML::Node.new("pos", doc)
|
34
|
+
pos['line'] = child.first.to_s
|
35
|
+
pos['column'] = child[1].to_s
|
36
|
+
xml_node.add_child(pos)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|