foodcritic 3.0.3 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +6 -14
- data/CHANGELOG.md +65 -0
- data/README.md +1 -1
- data/chef_dsl_metadata/chef_11.10.0.json +10272 -0
- data/chef_dsl_metadata/chef_11.10.2.json +10272 -0
- data/chef_dsl_metadata/chef_11.10.4.json +10272 -0
- data/chef_dsl_metadata/chef_11.6.2.json +9566 -0
- data/chef_dsl_metadata/chef_11.8.0.json +10074 -0
- data/chef_dsl_metadata/chef_11.8.2.json +10074 -0
- data/features/001_check_node_access.feature +60 -0
- data/features/003_check_for_chef_server.feature +5 -0
- data/features/006_check_file_mode.feature +1 -0
- data/features/022_check_for_dodgy_conditions_within_loop.feature +10 -0
- data/features/040_check_raw_git_usage.feature +6 -0
- data/features/047_check_for_attribute_assignment_without_precedence.feature +2 -0
- data/features/build_framework_support.feature +10 -0
- data/features/exclude_paths_to_lint.feature +19 -0
- data/features/show_lines_matched.feature +4 -4
- data/features/sort_warnings.feature +1 -1
- data/features/step_definitions/cookbook_steps.rb +117 -24
- data/features/support/command_helpers.rb +45 -12
- data/features/support/env.rb +5 -0
- data/lib/foodcritic/api.rb +122 -99
- data/lib/foodcritic/ast.rb +6 -7
- data/lib/foodcritic/chef.rb +24 -23
- data/lib/foodcritic/command_line.rb +49 -41
- data/lib/foodcritic/domain.rb +12 -10
- data/lib/foodcritic/dsl.rb +4 -7
- data/lib/foodcritic/error_checker.rb +0 -3
- data/lib/foodcritic/linter.rb +45 -43
- data/lib/foodcritic/notifications.rb +32 -32
- data/lib/foodcritic/output.rb +3 -6
- data/lib/foodcritic/rake_task.rb +9 -10
- data/lib/foodcritic/rules.rb +278 -240
- data/lib/foodcritic/template.rb +6 -13
- data/lib/foodcritic/version.rb +1 -1
- data/lib/foodcritic/xml.rb +1 -3
- data/man/foodcritic.1 +6 -2
- data/man/foodcritic.1.ronn +4 -1
- data/spec/foodcritic/linter_spec.rb +2 -2
- data/spec/regression/expected-output.txt +296 -1
- metadata +160 -138
@@ -1,9 +1,8 @@
|
|
1
1
|
module FoodCritic
|
2
|
-
|
3
2
|
# This module contains the logic for the parsing of
|
4
|
-
# [Chef Notifications]
|
3
|
+
# [Chef Notifications]
|
4
|
+
# (http://docs.opscode.com/resource_common.html#notifications).
|
5
5
|
module Notifications
|
6
|
-
|
7
6
|
# Extracts notification details from the provided AST, returning an
|
8
7
|
# array of notification hashes.
|
9
8
|
#
|
@@ -28,31 +27,33 @@ module FoodCritic
|
|
28
27
|
|
29
28
|
# Chef supports two styles of notification.
|
30
29
|
notified_resource = if new_style_notification?(notify)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
30
|
+
# `notifies :restart, "service[foo]"`
|
31
|
+
new_style_notification(notify)
|
32
|
+
else
|
33
|
+
# `notifies :restart, resources(service: "foo")`
|
34
|
+
old_style_notification(notify)
|
35
|
+
end
|
37
36
|
|
38
37
|
# Ignore if the notification was not parsed
|
39
38
|
next unless notified_resource
|
40
39
|
|
41
40
|
# Now merge the extract notification details with the attributes
|
42
41
|
# that are common to both styles of notification.
|
43
|
-
notified_resource.merge(
|
44
|
-
|
45
|
-
|
42
|
+
notified_resource.merge(
|
43
|
+
{
|
44
|
+
# The `:type` of notification: `:subscribes` or `:notifies`.
|
45
|
+
type: notification_type(notify),
|
46
46
|
|
47
|
-
|
48
|
-
|
47
|
+
# The `:style` of notification: `:new` or `:old`.
|
48
|
+
style: new_style_notification?(notify) ? :new : :old,
|
49
49
|
|
50
|
-
|
51
|
-
|
50
|
+
# The target resource action.
|
51
|
+
action: notification_action(notify),
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
# The notification timing. Either `:immediate` or `:delayed`.
|
54
|
+
timing: notification_timing(notify)
|
55
|
+
}
|
56
|
+
)
|
56
57
|
end.compact
|
57
58
|
end
|
58
59
|
|
@@ -61,7 +62,6 @@ module FoodCritic
|
|
61
62
|
# Extract the `:resource_name` and `:resource_type` from a new-style
|
62
63
|
# notification.
|
63
64
|
def new_style_notification(notify)
|
64
|
-
|
65
65
|
# Given `notifies :restart, "service[foo]"` the target is the
|
66
66
|
# `"service[foo]"` string portion.
|
67
67
|
target_path = 'args_add_block/args_add/descendant::
|
@@ -75,7 +75,7 @@ module FoodCritic
|
|
75
75
|
|
76
76
|
# Convert the captured resource type and name to symbols.
|
77
77
|
resource_type, resource_name =
|
78
|
-
match.captures.tap{|m| m[0] = m[0].to_sym}
|
78
|
+
match.captures.tap { |m| m[0] = m[0].to_sym }
|
79
79
|
|
80
80
|
# Normally the `resource_name` will be a simple string. However in the
|
81
81
|
# case where it has an embedded sub-expression then we will return the
|
@@ -86,7 +86,7 @@ module FoodCritic
|
|
86
86
|
resource_name =
|
87
87
|
notify.xpath('args_add_block/args_add/string_literal')
|
88
88
|
end
|
89
|
-
{:
|
89
|
+
{ resource_name: resource_name, resource_type: resource_type }
|
90
90
|
end
|
91
91
|
|
92
92
|
# Extract the `:resource_name` and `:resource_type` from an old-style
|
@@ -97,11 +97,12 @@ module FoodCritic
|
|
97
97
|
resource_name = resources.xpath('string_add[1][count(../
|
98
98
|
descendant::string_add) = 1]/tstring_content/@value').to_s
|
99
99
|
resource_name = resources if resource_name.empty?
|
100
|
-
{:
|
100
|
+
{ resource_name: resource_name, resource_type: resource_type }
|
101
101
|
end
|
102
102
|
|
103
103
|
def notification_timing(notify)
|
104
|
-
# The notification timing should be the last symbol
|
104
|
+
# The notification timing should be the last symbol
|
105
|
+
# on the notifies element.
|
105
106
|
timing = notify.xpath('args_add_block/args_add/symbol_literal[last()]/
|
106
107
|
symbol/ident[1]/@value')
|
107
108
|
if timing.empty?
|
@@ -109,12 +110,11 @@ module FoodCritic
|
|
109
110
|
:delayed
|
110
111
|
else
|
111
112
|
case timing.first.to_s.to_sym
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
else timing.first.to_s.to_sym
|
113
|
+
# Both forms are valid, but we return `:immediate` for both to avoid
|
114
|
+
# the caller having to recognise both.
|
115
|
+
when :immediately, :immediate then :immediate
|
116
|
+
# Pass the timing through unmodified if we don't recognise it.
|
117
|
+
else timing.first.to_s.to_sym
|
118
118
|
end
|
119
119
|
end
|
120
120
|
end
|
@@ -125,7 +125,8 @@ module FoodCritic
|
|
125
125
|
|
126
126
|
def notification_action(notify)
|
127
127
|
notify.xpath('descendant::symbol[1]/ident/@value |
|
128
|
-
descendant::dyna_symbol[1]/xstring_add/
|
128
|
+
descendant::dyna_symbol[1]/xstring_add/
|
129
|
+
tstring_content/@value').first.to_s.to_sym
|
129
130
|
end
|
130
131
|
|
131
132
|
def notification_nodes(ast, &block)
|
@@ -142,6 +143,5 @@ module FoodCritic
|
|
142
143
|
ast.xpath('descendant::method_add_arg[fcall/ident/
|
143
144
|
@value="resources"]/descendant::assoc_new')
|
144
145
|
end
|
145
|
-
|
146
146
|
end
|
147
147
|
end
|
data/lib/foodcritic/output.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'set'
|
2
2
|
|
3
3
|
module FoodCritic
|
4
|
-
|
5
4
|
# Default output showing a summary view.
|
6
5
|
class SummaryOutput
|
7
6
|
# Output a summary view only listing the matching rules, file and line
|
@@ -15,7 +14,6 @@ module FoodCritic
|
|
15
14
|
|
16
15
|
# Display rule matches with surrounding context.
|
17
16
|
class ContextOutput
|
18
|
-
|
19
17
|
# Output the review showing matching lines with context.
|
20
18
|
#
|
21
19
|
# @param [Review] review The review to output.
|
@@ -32,7 +30,7 @@ module FoodCritic
|
|
32
30
|
|
33
31
|
key_by_file_and_line(review).each do |fn, warnings|
|
34
32
|
print_fn.call fn
|
35
|
-
unless File.
|
33
|
+
unless File.exist?(fn)
|
36
34
|
print_rule.call warnings[1].to_a.join("\n")
|
37
35
|
next
|
38
36
|
end
|
@@ -52,7 +50,8 @@ module FoodCritic
|
|
52
50
|
# Find the first warning within our context
|
53
51
|
context_warns = context_set & warn_lines
|
54
52
|
next_warn = context_warns.min
|
55
|
-
# We may need to interrupt the trailing context
|
53
|
+
# We may need to interrupt the trailing context
|
54
|
+
# of a previous warning
|
56
55
|
next_warn = file.lineno if warn_lines.include? file.lineno
|
57
56
|
|
58
57
|
# Display a warning
|
@@ -120,7 +119,5 @@ module FoodCritic
|
|
120
119
|
puts text
|
121
120
|
end
|
122
121
|
end
|
123
|
-
|
124
122
|
end
|
125
|
-
|
126
123
|
end
|
data/lib/foodcritic/rake_task.rb
CHANGED
@@ -16,24 +16,23 @@ module FoodCritic
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def options
|
19
|
-
{
|
20
|
-
|
21
|
-
|
19
|
+
{
|
20
|
+
fail_tags: ['correctness'], # differs to default cmd-line behaviour
|
21
|
+
cookbook_paths: @files,
|
22
|
+
exclude_paths: ['test/**/*', 'spec/**/*', 'features/**/*'],
|
23
|
+
context: false,
|
22
24
|
}.merge(@options)
|
23
25
|
end
|
24
26
|
|
25
27
|
def define
|
26
|
-
desc
|
28
|
+
desc 'Lint Chef cookbooks' unless ::Rake.application.last_comment
|
27
29
|
task(name) do
|
28
30
|
result = FoodCritic::Linter.new.check(options)
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
fail result.to_s if result.failed?
|
31
|
+
printer = options[:context] ? ContextOutput.new : SummaryOutput.new
|
32
|
+
printer.output(result) if result.warnings.any?
|
33
|
+
abort if result.failed?
|
34
34
|
end
|
35
35
|
end
|
36
|
-
|
37
36
|
end
|
38
37
|
end
|
39
38
|
end
|
data/lib/foodcritic/rules.rb
CHANGED
@@ -1,16 +1,24 @@
|
|
1
1
|
# This file contains all of the rules that ship with foodcritic.
|
2
2
|
#
|
3
|
-
# * Foodcritic rules perform static code analysis - rather than the cookbook
|
4
|
-
# being loaded by the interpreter it is parsed into a tree (AST) that is
|
5
|
-
# passed to each rule.
|
3
|
+
# * Foodcritic rules perform static code analysis - rather than the cookbook
|
4
|
+
# code being loaded by the interpreter it is parsed into a tree (AST) that is
|
5
|
+
# then passed to each rule.
|
6
6
|
# * Rules can use a number of API functions that ship with foodcritic to make
|
7
7
|
# sense of the parse tree.
|
8
8
|
# * Rules can also use XPath to query the AST. A rule can consist of a XPath
|
9
9
|
# query only, as any nodes returned from a `recipe` block will be converted
|
10
10
|
# into warnings.
|
11
11
|
|
12
|
-
rule
|
13
|
-
|
12
|
+
rule 'FC001',
|
13
|
+
'Use strings in preference to symbols to access node attributes' do
|
14
|
+
tags %w(style attributes)
|
15
|
+
recipe do |ast|
|
16
|
+
attribute_access(ast, type: :symbol)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
rule 'FC002', 'Avoid string interpolation where not required' do
|
21
|
+
tags %w(style strings)
|
14
22
|
recipe do |ast|
|
15
23
|
ast.xpath(%q{//*[self::string_literal | self::assoc_new]/string_add[
|
16
24
|
count(descendant::string_embexpr) = 1 and
|
@@ -18,36 +26,36 @@ rule "FC002", "Avoid string interpolation where not required" do
|
|
18
26
|
end
|
19
27
|
end
|
20
28
|
|
21
|
-
rule
|
22
|
-
|
23
|
-
|
24
|
-
tags %w
|
25
|
-
recipe do |ast,filename|
|
26
|
-
unless checks_for_chef_solo?(ast)
|
29
|
+
rule 'FC003',
|
30
|
+
'Check whether you are running with chef server before using'\
|
31
|
+
' server-specific features' do
|
32
|
+
tags %w(portability solo)
|
33
|
+
recipe do |ast, filename|
|
34
|
+
unless checks_for_chef_solo?(ast) || chef_solo_search_supported?(filename)
|
27
35
|
searches(ast)
|
28
36
|
end
|
29
37
|
end
|
30
38
|
end
|
31
39
|
|
32
|
-
rule
|
33
|
-
tags %w
|
40
|
+
rule 'FC004', 'Use a service resource to start and stop services' do
|
41
|
+
tags %w(style services)
|
34
42
|
recipe do |ast|
|
35
|
-
find_resources(ast, :
|
43
|
+
find_resources(ast, type: 'execute').find_all do |cmd|
|
36
44
|
cmd_str = (resource_attribute(cmd, 'command') || resource_name(cmd)).to_s
|
37
45
|
(cmd_str.include?('/etc/init.d') || ['service ', '/sbin/service ',
|
38
46
|
'start ', 'stop ', 'invoke-rc.d '].any? do |service_cmd|
|
39
47
|
cmd_str.start_with?(service_cmd)
|
40
|
-
end) && %w
|
48
|
+
end) && %w(start stop restart reload).any? { |a| cmd_str.include?(a) }
|
41
49
|
end
|
42
50
|
end
|
43
51
|
end
|
44
52
|
|
45
|
-
rule
|
46
|
-
tags %w
|
53
|
+
rule 'FC005', 'Avoid repetition of resource declarations' do
|
54
|
+
tags %w(style)
|
47
55
|
recipe do |ast|
|
48
56
|
resources = find_resources(ast).map do |res|
|
49
|
-
resource_attributes(res).merge({:
|
50
|
-
:
|
57
|
+
resource_attributes(res).merge({ type: resource_type(res),
|
58
|
+
ast: res })
|
51
59
|
end.chunk do |res|
|
52
60
|
res[:type] +
|
53
61
|
res[:ast].xpath("ancestor::*[self::if | self::unless | self::elsif |
|
@@ -55,74 +63,80 @@ rule "FC005", "Avoid repetition of resource declarations" do
|
|
55
63
|
descendant::pos[position() = 1]").to_s +
|
56
64
|
res[:ast].xpath("ancestor::method_add_block/command[
|
57
65
|
ident/@value='action']/args_add_block/descendant::ident/@value").to_s
|
58
|
-
end.reject{|res| res[1].size < 3}
|
66
|
+
end.reject { |res| res[1].size < 3 }
|
59
67
|
resources.map do |cont_res|
|
60
68
|
first_resource = cont_res[1][0][:ast]
|
61
69
|
# we have contiguous resources of the same type, but do they share the
|
62
70
|
# same attributes?
|
63
71
|
sorted_atts = cont_res[1].map do |atts|
|
64
|
-
atts.delete_if{|k| k == :ast}.to_a.sort do |x,y|
|
72
|
+
atts.delete_if { |k| k == :ast }.to_a.sort do |x, y|
|
65
73
|
x.first.to_s <=> y.first.to_s
|
66
74
|
end
|
67
75
|
end
|
68
76
|
first_resource if sorted_atts.all? do |att|
|
69
|
-
(att - sorted_atts.inject{|atts,a| atts & a}).length == 1
|
77
|
+
(att - sorted_atts.inject { |atts, a| atts & a }).length == 1
|
70
78
|
end
|
71
79
|
end.compact
|
72
80
|
end
|
73
81
|
end
|
74
82
|
|
75
|
-
rule
|
76
|
-
|
77
|
-
|
83
|
+
rule 'FC006',
|
84
|
+
'Mode should be quoted or fully specified when '\
|
85
|
+
'setting file permissions' do
|
86
|
+
tags %w(correctness files)
|
78
87
|
recipe do |ast|
|
79
88
|
ast.xpath(%q{//ident[@value='mode']/parent::command/
|
80
|
-
descendant::int[string-length(@value) < 5
|
81
|
-
and
|
89
|
+
descendant::int[string-length(@value) < 5
|
90
|
+
and not(starts-with(@value, "0")
|
91
|
+
and string-length(@value) = 4)][count(ancestor::aref) = 0]/
|
92
|
+
ancestor::method_add_block})
|
82
93
|
end
|
83
94
|
end
|
84
95
|
|
85
|
-
rule
|
86
|
-
|
87
|
-
|
88
|
-
|
96
|
+
rule 'FC007', 'Ensure recipe dependencies are reflected '\
|
97
|
+
'in cookbook metadata' do
|
98
|
+
tags %w(correctness metadata)
|
99
|
+
recipe do |ast, filename|
|
100
|
+
metadata_path = Pathname.new(
|
89
101
|
File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
|
90
|
-
next unless File.
|
91
|
-
actual_included = included_recipes(ast, :
|
102
|
+
next unless File.exist? metadata_path
|
103
|
+
actual_included = included_recipes(ast, with_partial_names: false)
|
92
104
|
undeclared = actual_included.keys.map do |recipe|
|
93
105
|
recipe.split('::').first
|
94
106
|
end - [cookbook_name(filename)] -
|
95
107
|
declared_dependencies(read_ast(metadata_path))
|
96
108
|
actual_included.map do |recipe, include_stmts|
|
97
109
|
if undeclared.include?(recipe) ||
|
98
|
-
undeclared.any?{|u| recipe.start_with?("#{u}::")}
|
110
|
+
undeclared.any? { |u| recipe.start_with?("#{u}::") }
|
99
111
|
include_stmts
|
100
112
|
end
|
101
113
|
end.flatten.compact
|
102
114
|
end
|
103
115
|
end
|
104
116
|
|
105
|
-
rule
|
106
|
-
tags %w
|
107
|
-
metadata do |ast,filename|
|
108
|
-
{
|
109
|
-
|
110
|
-
|
111
|
-
|
117
|
+
rule 'FC008', 'Generated cookbook metadata needs updating' do
|
118
|
+
tags %w(style metadata)
|
119
|
+
metadata do |ast, filename|
|
120
|
+
{
|
121
|
+
'maintainer' => 'YOUR_COMPANY_NAME',
|
122
|
+
'maintainer_email' => 'YOUR_EMAIL'
|
123
|
+
}.map do |field, value|
|
124
|
+
ast.xpath(%Q(//command[ident/@value='#{field}']/
|
125
|
+
descendant::tstring_content[@value='#{value}']))
|
112
126
|
end
|
113
127
|
end
|
114
128
|
end
|
115
129
|
|
116
|
-
rule
|
117
|
-
tags %w
|
130
|
+
rule 'FC009', 'Resource attribute not recognised' do
|
131
|
+
tags %w(correctness)
|
118
132
|
recipe do |ast|
|
119
133
|
matches = []
|
120
|
-
resource_attributes_by_type(ast).each do |type,resources|
|
134
|
+
resource_attributes_by_type(ast).each do |type, resources|
|
121
135
|
resources.each do |resource|
|
122
136
|
resource.keys.map(&:to_sym).reject do |att|
|
123
137
|
resource_attribute?(type.to_sym, att)
|
124
138
|
end.each do |invalid_att|
|
125
|
-
matches << find_resources(ast, :
|
139
|
+
matches << find_resources(ast, type: type).find do |res|
|
126
140
|
resource_attributes(res).include?(invalid_att.to_s)
|
127
141
|
end
|
128
142
|
end
|
@@ -132,36 +146,36 @@ rule "FC009", "Resource attribute not recognised" do
|
|
132
146
|
end
|
133
147
|
end
|
134
148
|
|
135
|
-
rule
|
136
|
-
tags %w
|
149
|
+
rule 'FC010', 'Invalid search syntax' do
|
150
|
+
tags %w(correctness search)
|
137
151
|
recipe do |ast|
|
138
152
|
# This only works for literal search strings
|
139
|
-
literal_searches(ast).reject{|search| valid_query?(search['value'])}
|
153
|
+
literal_searches(ast).reject { |search| valid_query?(search['value']) }
|
140
154
|
end
|
141
155
|
end
|
142
156
|
|
143
|
-
rule
|
144
|
-
tags %w
|
157
|
+
rule 'FC011', 'Missing README in markdown format' do
|
158
|
+
tags %w(style readme)
|
145
159
|
cookbook do |filename|
|
146
|
-
unless File.
|
160
|
+
unless File.exist?(File.join(filename, 'README.md'))
|
147
161
|
[file_match(File.join(filename, 'README.md'))]
|
148
162
|
end
|
149
163
|
end
|
150
164
|
end
|
151
165
|
|
152
|
-
rule
|
153
|
-
tags %w
|
166
|
+
rule 'FC012', 'Use Markdown for README rather than RDoc' do
|
167
|
+
tags %w(style readme)
|
154
168
|
cookbook do |filename|
|
155
|
-
if File.
|
169
|
+
if File.exist?(File.join(filename, 'README.rdoc'))
|
156
170
|
[file_match(File.join(filename, 'README.rdoc'))]
|
157
171
|
end
|
158
172
|
end
|
159
173
|
end
|
160
174
|
|
161
|
-
rule
|
162
|
-
tags %w
|
175
|
+
rule 'FC013', 'Use file_cache_path rather than hard-coding tmp paths' do
|
176
|
+
tags %w(style files)
|
163
177
|
recipe do |ast|
|
164
|
-
find_resources(ast, :
|
178
|
+
find_resources(ast, type: 'remote_file').find_all do |download|
|
165
179
|
path = (resource_attribute(download, 'path') ||
|
166
180
|
resource_name(download)).to_s
|
167
181
|
path.start_with?('/tmp/')
|
@@ -169,47 +183,47 @@ rule "FC013", "Use file_cache_path rather than hard-coding tmp paths" do
|
|
169
183
|
end
|
170
184
|
end
|
171
185
|
|
172
|
-
rule
|
173
|
-
tags %w
|
186
|
+
rule 'FC014', 'Consider extracting long ruby_block to library' do
|
187
|
+
tags %w(style libraries)
|
174
188
|
recipe do |ast|
|
175
|
-
find_resources(ast, :
|
189
|
+
find_resources(ast, type: 'ruby_block').find_all do |rb|
|
176
190
|
lines = rb.xpath("descendant::fcall[ident/@value='block']/../../
|
177
|
-
descendant::*[@line]/@line").map{|n| n.value.to_i}.sort
|
178
|
-
(!
|
191
|
+
descendant::*[@line]/@line").map { |n| n.value.to_i }.sort
|
192
|
+
(!lines.empty?) && (lines.last - lines.first) > 15
|
179
193
|
end
|
180
194
|
end
|
181
195
|
end
|
182
196
|
|
183
|
-
rule
|
184
|
-
tags %w
|
185
|
-
applies_to {|version| version >= gem_version(
|
197
|
+
rule 'FC015', 'Consider converting definition to a LWRP' do
|
198
|
+
tags %w(style definitions lwrp)
|
199
|
+
applies_to { |version| version >= gem_version('0.7.12') }
|
186
200
|
cookbook do |dir|
|
187
201
|
Dir[File.join(dir, 'definitions', '*.rb')].reject do |entry|
|
188
202
|
['.', '..'].include? entry
|
189
|
-
end.map{|entry| file_match(entry)}
|
203
|
+
end.map { |entry| file_match(entry) }
|
190
204
|
end
|
191
205
|
end
|
192
206
|
|
193
|
-
rule
|
194
|
-
tags %w
|
195
|
-
applies_to {|version| version >= gem_version(
|
207
|
+
rule 'FC016', 'LWRP does not declare a default action' do
|
208
|
+
tags %w(correctness lwrp)
|
209
|
+
applies_to { |version| version >= gem_version('0.7.12') }
|
196
210
|
resource do |ast, filename|
|
197
211
|
unless ["//ident/@value='default_action'",
|
198
212
|
"//def/bodystmt/descendant::assign/
|
199
|
-
var_field/ivar/@value='@action'"].any? {|expr| ast.xpath(expr)}
|
213
|
+
var_field/ivar/@value='@action'"].any? { |expr| ast.xpath(expr) }
|
200
214
|
[file_match(filename)]
|
201
215
|
end
|
202
216
|
end
|
203
217
|
end
|
204
218
|
|
205
|
-
rule
|
206
|
-
tags %w
|
219
|
+
rule 'FC017', 'LWRP does not notify when updated' do
|
220
|
+
tags %w(correctness lwrp)
|
207
221
|
applies_to do |version|
|
208
|
-
version >= gem_version(
|
222
|
+
version >= gem_version('0.7.12')
|
209
223
|
end
|
210
224
|
provider do |ast, filename|
|
211
225
|
|
212
|
-
use_inline_resources = !
|
226
|
+
use_inline_resources = !ast.xpath('//*[self::vcall or self::var_ref]/ident
|
213
227
|
[@value="use_inline_resources"]').empty?
|
214
228
|
|
215
229
|
unless use_inline_resources
|
@@ -219,11 +233,11 @@ rule "FC017", "LWRP does not notify when updated" do
|
|
219
233
|
actions.reject do |action|
|
220
234
|
blk = action.xpath('ancestor::command[1]/
|
221
235
|
following-sibling::*[self::do_block or self::brace_block]')
|
222
|
-
empty = !
|
223
|
-
converge_by = !
|
236
|
+
empty = !blk.xpath('stmts_add/void_stmt').empty?
|
237
|
+
converge_by = !blk.xpath('descendant::*[self::command or self::fcall]
|
224
238
|
/ident[@value="converge_by"]').empty?
|
225
239
|
|
226
|
-
updated_by_last_action = !
|
240
|
+
updated_by_last_action = !blk.xpath('descendant::*[self::call or
|
227
241
|
self::command_call]/*[self::vcall or self::var_ref/ident/
|
228
242
|
@value="new_resource"]/../ident[@value="updated_by_last_action"]
|
229
243
|
').empty?
|
@@ -235,9 +249,9 @@ rule "FC017", "LWRP does not notify when updated" do
|
|
235
249
|
end
|
236
250
|
end
|
237
251
|
|
238
|
-
rule
|
239
|
-
tags %w
|
240
|
-
applies_to {|version| version >= gem_version(
|
252
|
+
rule 'FC018', 'LWRP uses deprecated notification syntax' do
|
253
|
+
tags %w(style lwrp deprecated)
|
254
|
+
applies_to { |version| version >= gem_version('0.9.10') }
|
241
255
|
provider do |ast|
|
242
256
|
ast.xpath("//assign/var_field/ivar[@value='@updated']").map do |class_var|
|
243
257
|
match(class_var)
|
@@ -246,37 +260,44 @@ rule "FC018", "LWRP uses deprecated notification syntax" do
|
|
246
260
|
end
|
247
261
|
end
|
248
262
|
|
249
|
-
rule
|
250
|
-
tags %w
|
263
|
+
rule 'FC019', 'Access node attributes in a consistent manner' do
|
264
|
+
tags %w(style attributes)
|
251
265
|
cookbook do |cookbook_dir|
|
252
266
|
asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].reject do |file|
|
253
|
-
relative_path = Pathname.new(file).relative_path_from(
|
267
|
+
relative_path = Pathname.new(file).relative_path_from(
|
268
|
+
Pathname.new(cookbook_dir))
|
254
269
|
relative_path.to_s.split(File::SEPARATOR).include?('spec')
|
255
270
|
end.map do |file|
|
256
|
-
{:
|
271
|
+
{ path: file, ast: read_ast(file) }
|
257
272
|
end
|
258
273
|
types = [:string, :symbol, :vivified].map do |type|
|
259
|
-
{
|
260
|
-
|
261
|
-
:
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
274
|
+
{
|
275
|
+
access_type: type, count: files.map do |file|
|
276
|
+
attribute_access(file[:ast], type: type, ignore_calls: true,
|
277
|
+
cookbook_dir: cookbook_dir, ignore: 'run_state').tap do |ast|
|
278
|
+
unless ast.empty?
|
279
|
+
(asts[type] ||= []) << { ast: ast, path: file[:path] }
|
280
|
+
end
|
281
|
+
end.size
|
282
|
+
end.inject(:+)
|
283
|
+
}
|
284
|
+
end.reject { |type| type[:count] == 0 }
|
268
285
|
if asts.size > 1
|
269
|
-
least_used = asts[types.min
|
286
|
+
least_used = asts[types.min do |a, b|
|
287
|
+
a[:count] <=> b[:count]
|
288
|
+
end[:access_type]]
|
270
289
|
least_used.map do |file|
|
271
|
-
file[:ast].map
|
290
|
+
file[:ast].map do |ast|
|
291
|
+
match(ast).merge(filename: file[:path])
|
292
|
+
end.flatten
|
272
293
|
end
|
273
294
|
end
|
274
295
|
end
|
275
296
|
end
|
276
297
|
|
277
|
-
rule
|
278
|
-
tags %w
|
279
|
-
applies_to {|version| version >= gem_version(
|
298
|
+
rule 'FC021', 'Resource condition in provider may not behave as expected' do
|
299
|
+
tags %w(correctness lwrp)
|
300
|
+
applies_to { |version| version >= gem_version('0.10.6') }
|
280
301
|
provider do |ast|
|
281
302
|
find_resources(ast).map do |resource|
|
282
303
|
condition = resource.xpath(%q{//method_add_block/
|
@@ -289,27 +310,34 @@ rule "FC021", "Resource condition in provider may not behave as expected" do
|
|
289
310
|
end
|
290
311
|
end
|
291
312
|
|
292
|
-
rule
|
293
|
-
tags %w
|
294
|
-
applies_to {|version| version >= gem_version(
|
313
|
+
rule 'FC022', 'Resource condition within loop may not behave as expected' do
|
314
|
+
tags %w(correctness)
|
315
|
+
applies_to { |version| version >= gem_version('0.10.6') }
|
295
316
|
recipe do |ast|
|
296
|
-
ast.xpath("//call[ident/@value='each']/../do_block
|
297
|
-
|
317
|
+
ast.xpath("//call[ident/@value='each']/../do_block[count(ancestor::
|
318
|
+
method_add_block/method_add_arg/fcall/ident[@value='only_if' or
|
319
|
+
@value = 'not_if']) = 0]").map do |lp|
|
320
|
+
block_vars = lp.xpath('block_var/params/child::*').map do |n|
|
298
321
|
n.name.sub(/^ident/, '')
|
299
|
-
end +
|
322
|
+
end + lp.xpath('block_var/params/child::*/descendant::ident').map do |v|
|
300
323
|
v['value']
|
301
324
|
end
|
302
|
-
find_resources(
|
325
|
+
find_resources(lp).map do |resource|
|
303
326
|
# if any of the parameters to the block are used in a condition then we
|
304
327
|
# have a match
|
305
328
|
unless (block_vars &
|
306
329
|
(resource.xpath(%q{descendant::ident[@value='not_if' or
|
307
330
|
@value='only_if']/ancestor::*[self::method_add_block or
|
308
|
-
self::command][1]/descendant::ident/@value}).map
|
331
|
+
self::command][1]/descendant::ident/@value}).map do |a|
|
332
|
+
a.value
|
333
|
+
end)).empty?
|
309
334
|
c = resource.xpath('command[count(descendant::string_embexpr) = 0]')
|
335
|
+
if resource.xpath('command/ident/@value').first.value == 'define'
|
336
|
+
next
|
337
|
+
end
|
310
338
|
resource unless c.empty? || block_vars.any? do |var|
|
311
|
-
!
|
312
|
-
var_ref/ident[@value='#{var}']
|
339
|
+
!resource.xpath(%Q(command/args_add_block/args_add/
|
340
|
+
var_ref/ident[@value='#{var}'])).empty?
|
313
341
|
end
|
314
342
|
end
|
315
343
|
end
|
@@ -317,8 +345,8 @@ rule "FC022", "Resource condition within loop may not behave as expected" do
|
|
317
345
|
end
|
318
346
|
end
|
319
347
|
|
320
|
-
rule
|
321
|
-
tags %w
|
348
|
+
rule 'FC023', 'Prefer conditional attributes' do
|
349
|
+
tags %w(style)
|
322
350
|
recipe do |ast|
|
323
351
|
ast.xpath(%q{//method_add_block[command/ident][count(descendant::ident
|
324
352
|
[@value='only_if' or @value='not_if']) = 0]/ancestor::*[self::if or
|
@@ -329,24 +357,27 @@ rule "FC023", "Prefer conditional attributes" do
|
|
329
357
|
end
|
330
358
|
end
|
331
359
|
|
332
|
-
rule
|
333
|
-
tags %w
|
334
|
-
RHEL = %w
|
360
|
+
rule 'FC024', 'Consider adding platform equivalents' do
|
361
|
+
tags %w(portability)
|
362
|
+
RHEL = %w(amazon centos redhat scientific)
|
335
363
|
recipe do |ast, filename|
|
336
364
|
next if Pathname.new(filename).basename.to_s == 'metadata.rb'
|
337
365
|
metadata_path = Pathname.new(
|
338
366
|
File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
|
339
|
-
md_platforms = if File.
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
367
|
+
md_platforms = if File.exist?(metadata_path)
|
368
|
+
supported_platforms(read_ast(
|
369
|
+
metadata_path)).map { |p| p[:platform] }
|
370
|
+
else
|
371
|
+
[]
|
372
|
+
end
|
344
373
|
md_platforms = RHEL if md_platforms.empty?
|
345
374
|
|
346
|
-
['//method_add_arg[fcall/ident/@value="platform?"]/
|
347
|
-
|
375
|
+
['//method_add_arg[fcall/ident/@value="platform?"]/
|
376
|
+
arg_paren/args_add_block',
|
377
|
+
'//when'].map do |expr|
|
348
378
|
ast.xpath(expr).map do |whn|
|
349
|
-
platforms = whn.xpath(
|
379
|
+
platforms = whn.xpath('args_add/
|
380
|
+
descendant::tstring_content').map do |p|
|
350
381
|
p['value']
|
351
382
|
end.sort
|
352
383
|
unless platforms.size == 1 || (md_platforms & platforms).empty?
|
@@ -358,16 +389,17 @@ rule "FC024", "Consider adding platform equivalents" do
|
|
358
389
|
end
|
359
390
|
end
|
360
391
|
|
361
|
-
rule
|
362
|
-
tags %w
|
363
|
-
applies_to {|version| version >= gem_version(
|
392
|
+
rule 'FC025', 'Prefer chef_gem to compile-time gem install' do
|
393
|
+
tags %w(style deprecated)
|
394
|
+
applies_to { |version| version >= gem_version('0.10.10') }
|
364
395
|
recipe do |ast|
|
365
396
|
gem_install = ast.xpath("//stmts_add/assign[method_add_block[command/ident/
|
366
397
|
@value='gem_package'][do_block/stmts_add/command[ident/@value='action']
|
367
398
|
[descendant::ident/@value='nothing']]]")
|
368
399
|
gem_install.map do |install|
|
369
|
-
gem_var = install.xpath(
|
370
|
-
unless ast.xpath("//method_add_arg[call/
|
400
|
+
gem_var = install.xpath('var_field/ident/@value')
|
401
|
+
unless ast.xpath("//method_add_arg[call/
|
402
|
+
var_ref/ident/@value='#{gem_var}']
|
371
403
|
[arg_paren/descendant::ident/@value='install' or
|
372
404
|
arg_paren/descendant::ident/@value='upgrade']").empty?
|
373
405
|
gem_install
|
@@ -376,34 +408,34 @@ rule "FC025", "Prefer chef_gem to compile-time gem install" do
|
|
376
408
|
end
|
377
409
|
end
|
378
410
|
|
379
|
-
rule
|
380
|
-
tags %w
|
381
|
-
applies_to {|version| version >= gem_version(
|
411
|
+
rule 'FC026', 'Conditional execution block attribute contains only string' do
|
412
|
+
tags %w(correctness)
|
413
|
+
applies_to { |version| version >= gem_version('0.7.4') }
|
382
414
|
recipe do |ast|
|
383
|
-
find_resources(ast).map{|r| resource_attributes(r)}.map do |resource|
|
415
|
+
find_resources(ast).map { |r| resource_attributes(r) }.map do |resource|
|
384
416
|
[resource['not_if'], resource['only_if']]
|
385
417
|
end.flatten.compact.select do |condition|
|
386
|
-
condition.respond_to?(:xpath)
|
387
|
-
!
|
388
|
-
!
|
418
|
+
condition.respond_to?(:xpath) &&
|
419
|
+
!condition.xpath('descendant::string_literal').empty? &&
|
420
|
+
!condition.xpath('stmts_add/string_literal').empty? &&
|
389
421
|
condition.xpath('descendant::stmts_add[count(ancestor::
|
390
422
|
string_literal) = 0]').size == 1
|
391
423
|
end
|
392
424
|
end
|
393
425
|
end
|
394
426
|
|
395
|
-
rule
|
396
|
-
tags %w
|
427
|
+
rule 'FC027', 'Resource sets internal attribute' do
|
428
|
+
tags %w(correctness)
|
397
429
|
recipe do |ast|
|
398
|
-
find_resources(ast, :
|
430
|
+
find_resources(ast, type: :service).map do |service|
|
399
431
|
service unless (resource_attributes(service).keys &
|
400
432
|
['enabled', 'running']).empty?
|
401
433
|
end.compact
|
402
434
|
end
|
403
435
|
end
|
404
436
|
|
405
|
-
rule
|
406
|
-
tags %w
|
437
|
+
rule 'FC028', 'Incorrect #platform? usage' do
|
438
|
+
tags %w(correctness)
|
407
439
|
recipe do |ast|
|
408
440
|
ast.xpath(%q{//*[self::call | self::command_call]
|
409
441
|
[(var_ref|vcall)/ident/@value='node']
|
@@ -411,44 +443,44 @@ rule "FC028", "Incorrect #platform? usage" do
|
|
411
443
|
end
|
412
444
|
end
|
413
445
|
|
414
|
-
rule
|
415
|
-
tags %w
|
416
|
-
metadata do |ast,filename|
|
446
|
+
rule 'FC029', 'No leading cookbook name in recipe metadata' do
|
447
|
+
tags %w(correctness metadata)
|
448
|
+
metadata do |ast, filename|
|
417
449
|
ast.xpath('//command[ident/@value="recipe"]').map do |declared_recipe|
|
418
450
|
next unless declared_recipe.xpath('count(//vcall|//var_ref)').to_i == 0
|
419
451
|
recipe_name = declared_recipe.xpath('args_add_block/
|
420
452
|
descendant::tstring_content[1]/@value').to_s
|
421
453
|
unless recipe_name.empty? ||
|
422
454
|
recipe_name.split('::').first == cookbook_name(filename.to_s)
|
423
|
-
|
455
|
+
declared_recipe
|
424
456
|
end
|
425
457
|
end.compact
|
426
458
|
end
|
427
459
|
end
|
428
460
|
|
429
|
-
rule
|
430
|
-
tags %w
|
461
|
+
rule 'FC030', 'Cookbook contains debugger breakpoints' do
|
462
|
+
tags %w(annoyances)
|
431
463
|
def pry_bindings(ast)
|
432
464
|
ast.xpath('//call[(vcall|var_ref)/ident/@value="binding"]
|
433
465
|
[ident/@value="pry"]')
|
434
466
|
end
|
435
|
-
recipe{|ast| pry_bindings(ast)}
|
436
|
-
library{|ast| pry_bindings(ast)}
|
437
|
-
metadata{|ast| pry_bindings(ast)}
|
438
|
-
template{|ast| pry_bindings(ast)}
|
467
|
+
recipe { |ast| pry_bindings(ast) }
|
468
|
+
library { |ast| pry_bindings(ast) }
|
469
|
+
metadata { |ast| pry_bindings(ast) }
|
470
|
+
template { |ast| pry_bindings(ast) }
|
439
471
|
end
|
440
472
|
|
441
|
-
rule
|
442
|
-
tags %w
|
473
|
+
rule 'FC031', 'Cookbook without metadata file' do
|
474
|
+
tags %w(correctness metadata)
|
443
475
|
cookbook do |filename|
|
444
|
-
if !
|
476
|
+
if !File.exist?(File.join(filename, 'metadata.rb'))
|
445
477
|
[file_match(File.join(filename, 'metadata.rb'))]
|
446
478
|
end
|
447
479
|
end
|
448
480
|
end
|
449
481
|
|
450
|
-
rule
|
451
|
-
tags %w
|
482
|
+
rule 'FC032', 'Invalid notification timing' do
|
483
|
+
tags %w(correctness notifications)
|
452
484
|
recipe do |ast|
|
453
485
|
find_resources(ast).select do |resource|
|
454
486
|
notifications(resource).any? do |notification|
|
@@ -458,16 +490,16 @@ rule "FC032", "Invalid notification timing" do
|
|
458
490
|
end
|
459
491
|
end
|
460
492
|
|
461
|
-
rule
|
462
|
-
tags %w
|
463
|
-
recipe do |ast,filename|
|
464
|
-
find_resources(ast, :
|
493
|
+
rule 'FC033', 'Missing template' do
|
494
|
+
tags %w(correctness)
|
495
|
+
recipe do |ast, filename|
|
496
|
+
find_resources(ast, type: :template).reject do |resource|
|
465
497
|
resource_attributes(resource)['local'] ||
|
466
498
|
resource_attributes(resource)['cookbook']
|
467
499
|
end.map do |resource|
|
468
500
|
file = template_file(resource_attributes(resource,
|
469
|
-
|
470
|
-
{:
|
501
|
+
return_expressions: true))
|
502
|
+
{ resource: resource, file: file }
|
471
503
|
end.reject do |resource|
|
472
504
|
resource[:file].respond_to?(:xpath)
|
473
505
|
end.select do |resource|
|
@@ -479,82 +511,85 @@ rule "FC033", "Missing template" do
|
|
479
511
|
end
|
480
512
|
File.join(relative_path.reverse) == resource[:file]
|
481
513
|
end
|
482
|
-
end.map{|resource| resource[:resource]}
|
514
|
+
end.map { |resource| resource[:resource] }
|
483
515
|
end
|
484
516
|
end
|
485
517
|
|
486
|
-
rule
|
487
|
-
tags %w
|
488
|
-
recipe do |ast,filename|
|
518
|
+
rule 'FC034', 'Unused template variables' do
|
519
|
+
tags %w(correctness)
|
520
|
+
recipe do |ast, filename|
|
489
521
|
Array(resource_attributes_by_type(ast)['template']).select do |t|
|
490
|
-
t['variables']
|
522
|
+
t['variables'] && t['variables'].respond_to?(:xpath)
|
491
523
|
end.map do |resource|
|
492
524
|
all_templates = template_paths(filename)
|
493
|
-
|
525
|
+
template_paths = all_templates.select do |path|
|
494
526
|
File.basename(path) == template_file(resource)
|
495
527
|
end
|
496
|
-
next unless
|
528
|
+
next unless template_paths.any?
|
497
529
|
passed_vars = resource['variables'].xpath(
|
498
|
-
'symbol/ident/@value').map{|tv| tv.to_s}
|
530
|
+
'symbol/ident/@value').map { |tv| tv.to_s }
|
499
531
|
|
500
|
-
|
501
|
-
|
502
|
-
|
532
|
+
unused_vars_exist = template_paths.all? do |template_path|
|
533
|
+
begin
|
534
|
+
template_vars = templates_included(
|
535
|
+
all_templates, template_path).map do |template|
|
503
536
|
read_ast(template).xpath('//var_ref/ivar/@value').map do |v|
|
504
537
|
v.to_s.sub(/^@/, '')
|
505
538
|
end
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
539
|
+
end.flatten
|
540
|
+
! (passed_vars - template_vars).empty?
|
541
|
+
rescue RecursedTooFarError
|
542
|
+
false
|
543
|
+
end
|
511
544
|
end
|
545
|
+
file_match(template_paths.first) if unused_vars_exist
|
512
546
|
end.compact
|
513
547
|
end
|
514
548
|
end
|
515
549
|
|
516
|
-
rule
|
517
|
-
tags %w
|
550
|
+
rule 'FC037', 'Invalid notification action' do
|
551
|
+
tags %w(correctness)
|
518
552
|
recipe do |ast|
|
519
553
|
find_resources(ast).select do |resource|
|
520
554
|
notifications(resource).any? do |n|
|
521
555
|
type = case n[:type]
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
n[:action].size > 0
|
556
|
+
when :notifies then n[:resource_type]
|
557
|
+
when :subscribes then resource_type(resource).to_sym
|
558
|
+
end
|
559
|
+
n[:action].size > 0 && !resource_action?(type, n[:action])
|
526
560
|
end
|
527
561
|
end
|
528
562
|
end
|
529
563
|
end
|
530
564
|
|
531
|
-
rule
|
532
|
-
tags %w
|
565
|
+
rule 'FC038', 'Invalid resource action' do
|
566
|
+
tags %w(correctness)
|
533
567
|
recipe do |ast|
|
534
568
|
find_resources(ast).select do |resource|
|
535
569
|
actions = resource_attributes(resource)['action']
|
536
570
|
if actions.respond_to?(:xpath)
|
537
|
-
actions = actions.xpath('descendant::array/
|
571
|
+
actions = actions.xpath('descendant::array/
|
572
|
+
descendant::symbol/ident/@value')
|
538
573
|
else
|
539
574
|
actions = Array(actions)
|
540
575
|
end
|
541
|
-
actions.reject{|a| a.to_s.empty?}.any? do |action|
|
542
|
-
!
|
576
|
+
actions.reject { |a| a.to_s.empty? }.any? do |action|
|
577
|
+
!resource_action?(resource_type(resource), action)
|
543
578
|
end
|
544
579
|
end
|
545
580
|
end
|
546
581
|
end
|
547
582
|
|
548
|
-
rule
|
549
|
-
tags %w
|
583
|
+
rule 'FC039', 'Node method cannot be accessed with key' do
|
584
|
+
tags %w(correctness)
|
550
585
|
recipe do |ast|
|
551
|
-
[{:
|
552
|
-
{:
|
553
|
-
attribute_access(ast, :
|
586
|
+
[{ type: :string, path: '@value' },
|
587
|
+
{ type: :symbol, path: 'ident/@value' }].map do |access_type|
|
588
|
+
attribute_access(ast, type: access_type[:type]).select do |att|
|
554
589
|
att_name = att.xpath(access_type[:path]).to_s.to_sym
|
555
590
|
att_name != :tags && chef_node_methods.include?(att_name)
|
556
591
|
end.select do |att|
|
557
|
-
!
|
592
|
+
!att.xpath('ancestor::args_add_block[position() = 1]
|
558
593
|
[preceding-sibling::vcall | preceding-sibling::var_ref]').empty?
|
559
594
|
end.select do |att|
|
560
595
|
att_type = att.xpath('ancestor::args_add_block[position() = 1]
|
@@ -565,100 +600,103 @@ rule "FC039", "Node method cannot be accessed with key" do
|
|
565
600
|
end
|
566
601
|
end
|
567
602
|
|
568
|
-
rule
|
569
|
-
tags %w
|
603
|
+
rule 'FC040', 'Execute resource used to run git commands' do
|
604
|
+
tags %w(style recipe etsy)
|
570
605
|
recipe do |ast|
|
571
|
-
possible_git_commands = %w
|
572
|
-
find_resources(ast, :
|
606
|
+
possible_git_commands = %w( clone fetch pull checkout reset )
|
607
|
+
find_resources(ast, type: 'execute').select do |cmd|
|
573
608
|
cmd_str = (resource_attribute(cmd, 'command') || resource_name(cmd)).to_s
|
574
609
|
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
!git_cmd.captures.nil? && possible_git_commands.include?(git_cmd.captures[0])
|
610
|
+
actual_git_commands = cmd_str.scan(/git ([a-z]+)/).map { |c| c.first }
|
611
|
+
(possible_git_commands & actual_git_commands).any?
|
579
612
|
end
|
580
613
|
end
|
581
614
|
end
|
582
615
|
|
583
|
-
rule
|
584
|
-
tags %w
|
616
|
+
rule 'FC041', 'Execute resource used to run curl or wget commands' do
|
617
|
+
tags %w(style recipe etsy)
|
585
618
|
recipe do |ast|
|
586
|
-
find_resources(ast, :
|
619
|
+
find_resources(ast, type: 'execute').select do |cmd|
|
587
620
|
cmd_str = (resource_attribute(cmd, 'command') || resource_name(cmd)).to_s
|
588
621
|
(cmd_str.include?('curl ') || cmd_str.include?('wget '))
|
589
622
|
end
|
590
623
|
end
|
591
624
|
end
|
592
625
|
|
593
|
-
rule
|
594
|
-
tags %w
|
626
|
+
rule 'FC042', 'Prefer include_recipe to require_recipe' do
|
627
|
+
tags %w(deprecated)
|
595
628
|
recipe do |ast|
|
596
629
|
ast.xpath('//command[ident/@value="require_recipe"]')
|
597
630
|
end
|
598
631
|
end
|
599
632
|
|
600
|
-
rule
|
601
|
-
tags %w
|
602
|
-
applies_to {|version| version >= gem_version(
|
633
|
+
rule 'FC043', 'Prefer new notification syntax' do
|
634
|
+
tags %w(style notifications deprecated)
|
635
|
+
applies_to { |version| version >= gem_version('0.9.10') }
|
603
636
|
recipe do |ast|
|
604
637
|
find_resources(ast).select do |resource|
|
605
|
-
notifications(resource).any?{|notify| notify[:style] == :old}
|
638
|
+
notifications(resource).any? { |notify| notify[:style] == :old }
|
606
639
|
end
|
607
640
|
end
|
608
641
|
end
|
609
642
|
|
610
|
-
rule
|
611
|
-
tags %w
|
643
|
+
rule 'FC044', 'Avoid bare attribute keys' do
|
644
|
+
tags %w(style)
|
612
645
|
attributes do |ast|
|
613
|
-
declared = ast.xpath('//descendant::var_field/ident/@value').map
|
646
|
+
declared = ast.xpath('//descendant::var_field/ident/@value').map do |v|
|
647
|
+
v.to_s
|
648
|
+
end
|
614
649
|
ast.xpath('//assign/*[self::vcall or self::var_ref]
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
650
|
+
[count(child::kw) = 0]/ident').select do |v|
|
651
|
+
(v['value'] != 'secure_password') &&
|
652
|
+
!declared.include?(v['value']) &&
|
653
|
+
!v.xpath("ancestor::*[self::brace_block or self::do_block]/block_var/
|
654
|
+
descendant::ident/@value='#{v['value']}'")
|
620
655
|
end
|
621
656
|
end
|
622
657
|
end
|
623
658
|
|
624
|
-
rule
|
625
|
-
tags %w
|
659
|
+
rule 'FC045', 'Consider setting cookbook name in metadata' do
|
660
|
+
tags %w(annoyances metadata)
|
626
661
|
metadata do |ast, filename|
|
627
662
|
unless ast.xpath('descendant::stmts_add/command/ident/@value="name"')
|
628
663
|
[file_match(filename)]
|
629
664
|
end
|
630
665
|
end
|
631
666
|
cookbook do |filename|
|
632
|
-
if !
|
667
|
+
if !File.exist?(File.join(filename, 'metadata.rb'))
|
633
668
|
[file_match(File.join(filename, 'metadata.rb'))]
|
634
669
|
end
|
635
670
|
end
|
636
671
|
end
|
637
672
|
|
638
|
-
rule
|
673
|
+
rule 'FC046', 'Attribute assignment uses assign unless nil' do
|
639
674
|
attributes do |ast|
|
640
|
-
attribute_access(ast).map
|
675
|
+
attribute_access(ast).map do |a|
|
676
|
+
a.xpath('ancestor::opassign/op[@value="||="]')
|
677
|
+
end
|
641
678
|
end
|
642
679
|
end
|
643
680
|
|
644
|
-
rule
|
645
|
-
tags %w
|
681
|
+
rule 'FC047', 'Attribute assignment does not specify precedence' do
|
682
|
+
tags %w(attributes correctness)
|
646
683
|
recipe do |ast|
|
647
684
|
attribute_access(ast).map do |att|
|
648
685
|
exclude_att_types = '[count(following-sibling::ident[
|
649
686
|
is_att_type(@value) or @value = "run_state"]) = 0]'
|
650
|
-
att.xpath(%Q
|
687
|
+
att.xpath(%Q(ancestor::assign[*[self::field | self::aref_field]
|
651
688
|
[descendant::*[self::vcall | self::var_ref][ident/@value="node"]
|
652
|
-
#{exclude_att_types}]]
|
653
|
-
att.xpath(%Q{ancestor::binary[@value="<<"]/*[position() = 1]
|
689
|
+
#{exclude_att_types}]]), AttFilter.new) +
|
690
|
+
att.xpath(%Q{ancestor::binary[@value="<<"]/*[position() = 1]
|
691
|
+
[self::aref]
|
654
692
|
[descendant::*[self::vcall | self::var_ref]#{exclude_att_types}
|
655
693
|
/ident/@value="node"]}, AttFilter.new)
|
656
694
|
end
|
657
695
|
end
|
658
696
|
end
|
659
697
|
|
660
|
-
rule
|
661
|
-
tags %w
|
698
|
+
rule 'FC048', 'Prefer Mixlib::ShellOut' do
|
699
|
+
tags %w(style processes)
|
662
700
|
recipe do |ast|
|
663
701
|
ast.xpath('//xstring_literal | //*[self::command or self::fcall]/
|
664
702
|
ident[@value="system"][count(following-sibling::args_add_block/
|
@@ -666,29 +704,29 @@ rule "FC048", "Prefer Mixlib::ShellOut" do
|
|
666
704
|
end
|
667
705
|
end
|
668
706
|
|
669
|
-
rule
|
670
|
-
tags %w
|
707
|
+
rule 'FC049', 'Role name does not match containing file name' do
|
708
|
+
tags %w(style roles)
|
671
709
|
role do |ast, filename|
|
672
710
|
role_name_specified = field_value(ast, :name)
|
673
711
|
role_name_file = Pathname.new(filename).basename.sub_ext('').to_s
|
674
|
-
if role_name_specified
|
712
|
+
if role_name_specified && role_name_specified != role_name_file
|
675
713
|
field(ast, :name)
|
676
714
|
end
|
677
715
|
end
|
678
716
|
end
|
679
717
|
|
680
|
-
rule
|
681
|
-
tags %w
|
718
|
+
rule 'FC050', 'Name includes invalid characters' do
|
719
|
+
tags %w(correctness environments roles)
|
682
720
|
def invalid_name(ast)
|
683
721
|
field(ast, :name) unless field_value(ast, :name) =~ /^[a-zA-Z0-9_\-]+$/
|
684
722
|
end
|
685
|
-
environment{|ast| invalid_name(ast)}
|
686
|
-
role{|ast| invalid_name(ast)}
|
723
|
+
environment { |ast| invalid_name(ast) }
|
724
|
+
role { |ast| invalid_name(ast) }
|
687
725
|
end
|
688
726
|
|
689
|
-
rule
|
690
|
-
tags %w
|
691
|
-
recipe do |_,filename|
|
727
|
+
rule 'FC051', 'Template partials loop indefinitely' do
|
728
|
+
tags %w(correctness)
|
729
|
+
recipe do |_, filename|
|
692
730
|
cbk_templates = template_paths(filename)
|
693
731
|
|
694
732
|
cbk_templates.select do |template|
|
@@ -698,6 +736,6 @@ rule "FC051", "Template partials loop indefinitely" do
|
|
698
736
|
rescue RecursedTooFarError
|
699
737
|
true
|
700
738
|
end
|
701
|
-
end.map{|t| file_match(t)}
|
739
|
+
end.map { |t| file_match(t) }
|
702
740
|
end
|
703
741
|
end
|