foodcritic 0.2.0 → 0.3.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.
@@ -1,89 +1,91 @@
1
+ require 'nokogiri'
2
+ require 'xmlsimple'
3
+
1
4
  module FoodCritic
2
5
 
3
6
  # Helper methods that form part of the Rules DSL.
4
7
  module Helpers
5
8
 
6
- # Given an AST type and parsed tree, return the matching subset.
9
+ # Create a match from the specified node.
7
10
  #
8
- # @param [Symbol] type The type of AST node to look for
9
- # @param [Array] node The parsed AST (or part there-of)
10
- # @return [Array] Matching nodes
11
- def ast(type, node)
12
- result = []
13
- result = [node] if node.first == type
14
- node.each { |n| result += ast(type, n) if n.respond_to?(:each) }
15
- result
11
+ # @param [Nokogiri::XML::Node] node The node to create a match for
12
+ # @return [Hash] Hash with the matched node name and position with the recipe
13
+ def match(node)
14
+ pos = node.xpath('descendant::pos').first
15
+ {:matched => node.name, :line => pos['line'], :column => pos['column']}
16
16
  end
17
17
 
18
18
  # Does the specified recipe check for Chef Solo?
19
19
  #
20
- # @param [Array] ast The AST of the cookbook recipe to check.
20
+ # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
21
21
  # @return [Boolean] True if there is a test for Chef::Config[:solo] in the recipe
22
22
  def checks_for_chef_solo?(ast)
23
- arefs = ast(:aref, ast)
24
- arefs.any? do |aref|
25
- ast(:@const, aref).map { |const| const[1] } == ['Chef', 'Config'] and
26
- ast(:@ident, ast(:symbol, aref)).map { |sym| sym.drop(1).first }.include? 'solo'
27
- end
23
+ ! ast.xpath(%q{//if/aref[count(descendant::const[@value = 'Chef' or @value = 'Config']) = 2 and
24
+ count(descendant::ident[@value='solo']) > 0]}).empty?
25
+ end
26
+
27
+ # Searches performed by the specified recipe.
28
+ #
29
+ # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
30
+ # @return [Boolean] True if the recipe performs a search
31
+ def searches(ast)
32
+ ast.xpath("//fcall/ident[@value = 'search']")
28
33
  end
29
34
 
30
35
  # Find Chef resources of the specified type.
31
36
  # TODO: Include blockless resources
32
37
  #
33
- # @param [Array] ast The AST of the cookbook recipe to check
38
+ # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check
34
39
  # @param [String] type The type of resource to look for (or nil for all resources)
35
40
  def find_resources(ast, type = nil)
36
- ast(:method_add_block, ast).find_all do |resource|
37
- resource[1][0] == :command and resource[1][1][0] == :@ident and (type.nil? || resource[1][1][1] == type)
38
- end
41
+ ast.xpath(%Q{//method_add_block[command/ident#{type.nil? ? '' : "[@value='#{type}']"}]})
39
42
  end
40
43
 
41
44
  # Return the type, e.g. 'package' for a given resource
42
45
  #
43
- # @param [Array] resource The resource AST
46
+ # @param [Nokogiri::XML::Node] resource The resource AST
44
47
  # @return [String] The type of resource
45
48
  def resource_type(resource)
46
- resource[1][1][1]
49
+ resource.xpath('string(command/ident/@value)')
47
50
  end
48
51
 
49
52
  # Retrieve the name attribute associated with the specified resource.
50
53
  #
51
- # @param [Array] resource The resource AST to lookup the name attribute under
54
+ # @param [Nokogiri::XML::Node] resource The resource AST to lookup the name attribute under
52
55
  def resource_name(resource)
53
- ast(:@tstring_content, resource[1]).flatten[1]
56
+ resource.xpath('string(command//tstring_content/@value)')
54
57
  end
55
58
 
56
59
  # Retrieve a single-valued attribute from the specified resource.
57
60
  #
58
61
  # @param [String] name The attribute name
59
- # @param [Array] resource The resource AST to lookup the attribute under
62
+ # @param [Nokogiri::XML::Node] resource The resource AST to lookup the attribute under
60
63
  # @return [String] The attribute value for the specified attribute
61
64
  def resource_attribute(name, resource)
62
- cmd = ast(:command, ast(:do_block, resource))
63
- atts = cmd.find_all { |att| ast(:@ident, att).flatten.drop(1).first == name }
64
- value = ast(:@tstring_content, atts).flatten.drop(1)
65
- unless value.empty?
66
- return value.first
67
- end
68
- nil
65
+ resource_attributes(resource)[name]
69
66
  end
70
67
 
71
68
  # Retrieve all attributes from the specified resource.
72
69
  #
73
- # @param [Array] resource The resource AST
70
+ # @param [Nokogiri::XML::Node] resource The resource AST
74
71
  # @return [Hash] The resource attributes
75
72
  def resource_attributes(resource)
76
73
  atts = {:name => resource_name(resource)}
77
- ast(:command, ast(:do_block, resource)).find_all{|cmd| cmd.first == :command}.each do |cmd|
78
- atts[cmd[1][1]] = ast(:@tstring_content, cmd[2]).flatten[1] || ast(:@ident, cmd[2]).flatten[1]
74
+ resource.xpath('do_block/descendant::command').each do |att|
75
+ if att.xpath('descendant::symbol').empty?
76
+ att_value = att.xpath('string(descendant::tstring_content/@value)')
77
+ else
78
+ att_value = att.xpath('string(descendant::symbol/ident/@value)').to_sym
79
+ end
80
+ atts[att.xpath('string(ident/@value)')] = att_value
79
81
  end
80
82
  atts
81
83
  end
82
84
 
83
85
  # Retrieve all resources of a given type
84
86
  #
85
- # @param [Array] ast The recipe AST
86
- # @return [Array] The matching resources
87
+ # @param [Nokogiri::XML::Node] ast The recipe AST
88
+ # @return [Hash] The matching resources
87
89
  def resources_by_type(ast)
88
90
  result = Hash.new{|hash, key| hash[key] = Array.new}
89
91
  find_resources(ast).each{|resource| result[resource_type(resource)] << resource}
@@ -92,7 +94,7 @@ module FoodCritic
92
94
 
93
95
  # Retrieve the attributes as a hash for all resources of a given type.
94
96
  #
95
- # @param [Array] ast The recipe AST
97
+ # @param [Nokogiri::XML::Node] ast The recipe AST
96
98
  # @return [Hash] An array of resource attributes keyed by type.
97
99
  def resource_attributes_by_type(ast)
98
100
  result = {}
@@ -101,6 +103,77 @@ module FoodCritic
101
103
  end
102
104
  result
103
105
  end
106
+
107
+ # Retrieve the recipes that are included within the given recipe AST.
108
+ #
109
+ # @param [Nokogiri::XML::Node] ast The recipe AST
110
+ # @return [Hash] include_recipe nodes keyed by included recipe name
111
+ def included_recipes(ast)
112
+ # we only support literal strings, ignoring sub-expressions
113
+ included = ast.xpath(%q{//command[ident/@value = 'include_recipe' and count(descendant::string_embexpr) = 0]/
114
+ descendant::tstring_content})
115
+ Hash[included.map{|recipe|recipe['value']}.zip(included)]
116
+ end
117
+
118
+ # The name of the cookbook containing the specified file.
119
+ #
120
+ # @param [String] file The file in the cookbook
121
+ # @return [String] The name of the containing cookbook
122
+ def cookbook_name(file)
123
+ File.basename(File.absolute_path(File.join(File.dirname(file), '..')))
124
+ end
125
+
126
+ # The dependencies declared in cookbook metadata.
127
+ #
128
+ # @param [Nokogiri::XML::Node] ast The metadata rb AST
129
+ # @return [Array] List of cookbooks depended on
130
+ def declared_dependencies(ast)
131
+ deps = ast.xpath("//command[ident/@value='depends']/descendant::args_add/descendant::tstring_content")
132
+ # handle quoted word arrays
133
+ var_ref = ast.xpath("//command[ident/@value='depends']/descendant::var_ref/ident")
134
+ deps += ast.xpath(%Q{//block_var/params/ident#{var_ref.first['value']}/ancestor::method_add_block/
135
+ call/descendant::tstring_content}) unless var_ref.empty?
136
+ deps.map{|dep| dep['value']}
137
+ end
138
+
139
+ # If the provided node is the line / column information.
140
+ #
141
+ # @param [Nokogiri::XML::Node] node A node within the AST
142
+ # @return [Boolean] True if this node holds the position data
143
+ def position_node?(node)
144
+ node.respond_to?(:length) and node.length == 2 and node.respond_to?(:all?) and node.all?{|child| child.respond_to?(:to_i)}
145
+ end
146
+
147
+ # Recurse the nested arrays provided by Ripper to create an intermediate Hash for ease of searching.
148
+ #
149
+ # @param [Nokogiri::XML::Node] node The AST
150
+ # @return [Hash] The friendlier Hash.
151
+ def ast_to_hash(node)
152
+ result = {}
153
+ if node.respond_to?(:each)
154
+ node.drop(1).each do |child|
155
+ if position_node?(child)
156
+ result[:pos] = {:line => child.first, :column => child[1]}
157
+ else
158
+ if child.respond_to?(:first)
159
+ result[child.first.to_s.gsub(/[^a-z_]/, '')] = ast_to_hash(child)
160
+ else
161
+ result[:value] = child unless child.nil?
162
+ end
163
+ end
164
+ end
165
+ end
166
+ result
167
+ end
168
+
169
+ # Read the AST for the given Ruby file
170
+ #
171
+ # @param [String] file The file to read
172
+ # @return [Nokogiri::XML::Node] The recipe AST
173
+ def read_file(file)
174
+ Nokogiri::XML(XmlSimple.xml_out(ast_to_hash(Ripper::SexpBuilder.new(IO.read(file)).parse)))
175
+ end
176
+
104
177
  end
105
178
 
106
179
  end
@@ -5,6 +5,8 @@ module FoodCritic
5
5
  # The main entry point for linting your Chef cookbooks.
6
6
  class Linter
7
7
 
8
+ include FoodCritic::Helpers
9
+
8
10
  # Create a new Linter, loading any defined rules.
9
11
  def initialize
10
12
  load_rules
@@ -17,11 +19,10 @@ module FoodCritic
17
19
  def check(cookbook_path)
18
20
  warnings = []
19
21
  files_to_process(cookbook_path).each do |file|
20
- ast = Ripper::SexpBuilder.new(IO.read(file)).parse
22
+ ast = read_file(file)
21
23
  @rules.each do |rule|
22
- rule.recipe.yield(ast).each do |match|
23
- warnings << Warning.new(rule, match.merge({:filename => file}))
24
- end
24
+ matches = rule.recipe.yield(ast, File.expand_path(file))
25
+ matches.each{|match| warnings << Warning.new(rule, match.merge({:filename => file}))} unless matches.nil?
25
26
  end
26
27
  end
27
28
  Review.new(warnings)
@@ -1,48 +1,25 @@
1
1
  rule "FC002", "Avoid string interpolation where not required" do
2
2
  description "When setting a resource value avoid string interpolation where not required."
3
3
  recipe do |ast|
4
- matches = []
5
- self.ast(:string_literal, ast).each do |literal|
6
- embed_expr = self.ast(:string_embexpr, literal)
7
- if embed_expr.size == 1
8
- literal[1].reject! { |expr| expr == embed_expr.first }
9
- if self.ast(:@tstring_content, literal).empty?
10
- self.ast(:@ident, embed_expr).map { |ident| ident.flatten.drop(1) }.each do |ident|
11
- matches << {:matched => ident[0], :line => ident[1], :column => ident[2]}
12
- end
13
- end
14
- end
15
- end
16
- matches
4
+ ast.xpath(%q{//string_literal[count(descendant::string_embexpr) = 1 and
5
+ count(string_add/tstring_content|string_add/string_add/tstring_content) = 0]}).map{|str| match(str)}
17
6
  end
18
7
  end
19
8
 
20
9
  rule "FC003", "Check whether you are running with chef server before using server-specific features" do
21
10
  description "Ideally your cookbooks should be usable without requiring chef server."
22
11
  recipe do |ast|
23
- matches = []
24
- function_calls = self.ast(:@ident, self.ast(:fcall, ast)).map { |fcall| fcall.drop(1).flatten }
25
- searches = function_calls.find_all { |fcall| fcall.first == 'search' }
26
- unless searches.empty? || checks_for_chef_solo?(ast)
27
- searches.each { |s| matches << {:matched => s[0], :line => s[1], :column => s[2]} }
28
- end
29
- matches
12
+ checks_for_chef_solo?(ast) ? [] : searches(ast).map{|s| match(s)}
30
13
  end
31
14
  end
32
15
 
33
16
  rule "FC004", "Use a service resource to start and stop services" do
34
17
  description "Avoid use of execute to control services - use the service resource instead."
35
18
  recipe do |ast|
36
- matches = []
37
19
  find_resources(ast, 'execute').find_all do |cmd|
38
- cmd_str = resource_attribute('command', cmd)
39
- cmd_str = resource_name(cmd) if cmd_str.nil?
20
+ cmd_str = (resource_attribute('command', cmd) || resource_name(cmd)).to_s
40
21
  cmd_str.include?('/etc/init.d') || cmd_str.start_with?('service ') || cmd_str.start_with?('/sbin/service ')
41
- end.each do |service_cmd|
42
- exec = ast(:@ident, service_cmd).first.drop(1).flatten
43
- matches << {:matched => exec[0], :line => exec[1], :column => exec[2]}
44
- end
45
- matches
22
+ end.map{|cmd| match(cmd)}
46
23
  end
47
24
  end
48
25
 
@@ -54,10 +31,30 @@ rule "FC005", "Avoid repetition of resource declarations" do
54
31
  resource_attributes_by_type(ast).each do |type, resource_atts|
55
32
  sorted_atts = resource_atts.map{|atts| atts.to_a.sort{|x,y| x.first.to_s <=> y.first.to_s }}
56
33
  if sorted_atts.all?{|att| (att - sorted_atts.inject{|atts,a| atts & a}).length == 1}
57
- first_resource = ast(:@ident, find_resources(ast, type).first).first[2]
58
- matches << {:matched => type, :line => first_resource[0], :column => first_resource[1]}
34
+ matches << match(find_resources(ast, type).first)
59
35
  end
60
36
  end
61
37
  matches
62
38
  end
63
- end
39
+ end
40
+
41
+ rule "FC006", "Mode should be quoted or fully specified when setting file permissions" do
42
+ description "Not quoting mode when setting permissions can lead to incorrect permissions being set."
43
+ recipe do |ast|
44
+ ast.xpath(%q{//ident[@value='mode']/parent::command/descendant::int[string-length(@value) < 4]/
45
+ ancestor::method_add_block}).map{|resource| match(resource)}
46
+ end
47
+ end
48
+
49
+ rule "FC007", "Ensure recipe dependencies are reflected in cookbook metadata" do
50
+ description "You are including a recipe that is not in the current cookbook and not defined as a dependency in your cookbook metadata."
51
+ recipe do |ast,filename|
52
+ metadata_path = File.join(File.dirname(filename), '..', 'metadata.rb')
53
+ next unless File.exists? metadata_path
54
+ undeclared = included_recipes(ast).keys.map{|recipe|recipe.split('::').first} - [cookbook_name(filename)] -
55
+ declared_dependencies(read_file(metadata_path))
56
+ included_recipes(ast).map do |recipe, resource|
57
+ match(resource) if undeclared.include?(recipe) || undeclared.any?{|u| recipe.start_with?("#{u}::")}
58
+ end.compact
59
+ end
60
+ end
@@ -1,3 +1,3 @@
1
1
  module FoodCritic
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
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: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,30 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-01 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2011-12-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nokogiri
16
+ requirement: &2157802560 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.5.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2157802560
25
+ - !ruby/object:Gem::Dependency
26
+ name: xml-simple
27
+ requirement: &2157800260 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.1
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2157800260
14
36
  description: Lint tool for Opscode Chef cookbooks.
15
37
  email:
16
38
  executables:
@@ -18,13 +40,13 @@ executables:
18
40
  extensions: []
19
41
  extra_rdoc_files: []
20
42
  files:
21
- - lib/foodcritic.rb
22
43
  - lib/foodcritic/domain.rb
44
+ - lib/foodcritic/dsl.rb
45
+ - lib/foodcritic/helpers.rb
23
46
  - lib/foodcritic/linter.rb
24
47
  - lib/foodcritic/rules.rb
25
- - lib/foodcritic/helpers.rb
26
48
  - lib/foodcritic/version.rb
27
- - lib/foodcritic/dsl.rb
49
+ - lib/foodcritic.rb
28
50
  - bin/foodcritic
29
51
  homepage: http://acrmp.github.com/foodcritic
30
52
  licenses:
@@ -47,12 +69,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
69
  version: '0'
48
70
  segments:
49
71
  - 0
50
- hash: 1647657478747687717
72
+ hash: 927165501432344600
51
73
  requirements: []
52
74
  rubyforge_project:
53
75
  rubygems_version: 1.8.10
54
76
  signing_key:
55
77
  specification_version: 3
56
- summary: foodcritic-0.2.0
78
+ summary: foodcritic-0.3.0
57
79
  test_files: []
58
80
  has_rdoc: