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.
- data/lib/foodcritic/helpers.rb +110 -37
- data/lib/foodcritic/linter.rb +5 -4
- data/lib/foodcritic/rules.rb +28 -31
- data/lib/foodcritic/version.rb +1 -1
- metadata +30 -8
data/lib/foodcritic/helpers.rb
CHANGED
|
@@ -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
|
-
#
|
|
9
|
+
# Create a match from the specified node.
|
|
7
10
|
#
|
|
8
|
-
# @param [
|
|
9
|
-
# @
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 [
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 [
|
|
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(
|
|
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 [
|
|
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
|
|
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 [
|
|
54
|
+
# @param [Nokogiri::XML::Node] resource The resource AST to lookup the name attribute under
|
|
52
55
|
def resource_name(resource)
|
|
53
|
-
|
|
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 [
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
78
|
-
|
|
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 [
|
|
86
|
-
# @return [
|
|
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 [
|
|
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
|
data/lib/foodcritic/linter.rb
CHANGED
|
@@ -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 =
|
|
22
|
+
ast = read_file(file)
|
|
21
23
|
@rules.each do |rule|
|
|
22
|
-
rule.recipe.yield(ast)
|
|
23
|
-
|
|
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)
|
data/lib/foodcritic/rules.rb
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
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: 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-
|
|
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
|
|
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:
|
|
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.
|
|
78
|
+
summary: foodcritic-0.3.0
|
|
57
79
|
test_files: []
|
|
58
80
|
has_rdoc:
|