foodcritic 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|