foodcritic 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
- cookbook do |filename|
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
- md.xpath(%Q{//command[ident/@value='#{field}']/
111
- descendant::tstring_content[@value='#{value}']}).map do |m|
112
- match(m).merge(:filename => metadata_path)
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
- 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|
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.map {|m| match(m).merge(:filename => metadata_path.to_s) }
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
- 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
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
@@ -1,4 +1,4 @@
1
1
  module FoodCritic
2
2
  # The current version of foodcritic
3
- VERSION = '1.4.0'
3
+ VERSION = '1.5.0'
4
4
  end
@@ -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