foodcritic 0.5.2 → 0.6.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.
@@ -32,13 +32,15 @@ module FoodCritic
32
32
  #
33
33
  # @return [String] Review as a string, this representation is liable to change.
34
34
  def to_s
35
- @warnings.map { |w| "#{w.rule.code}: #{w.rule.name}: #{w.match[:filename]}:#{w.match[:line]}" }.sort.uniq.join("\n")
35
+ @warnings.map{|w|["#{w.rule.code}: #{w.rule.name}: #{w.match[:filename]}", w.match[:line].to_i]}.sort do |x,y|
36
+ x.first == y.first ? x[1] <=> y[1] : x.first <=> y.first
37
+ end.map{|w|"#{w.first}:#{w[1]}"}.uniq.join("\n")
36
38
  end
37
39
  end
38
40
 
39
41
  # A rule to be matched against.
40
42
  class Rule
41
- attr_accessor :code, :name, :description, :recipe, :tags
43
+ attr_accessor :code, :name, :description, :cookbook, :recipe, :provider, :tags
42
44
 
43
45
  # Create a new rule
44
46
  #
@@ -40,6 +40,20 @@ module FoodCritic
40
40
  rules.last.recipe = block
41
41
  end
42
42
 
43
+ # Define a matcher that will be passed the AST with this method.
44
+ #
45
+ # @param [block] block Your implemented matcher that returns a match Hash.
46
+ def provider(&block)
47
+ rules.last.provider = block
48
+ end
49
+
50
+ # Define a matcher that will be passed the cookbook path with this method.
51
+ #
52
+ # @param [block] block Your implemented matcher that returns a match Hash.
53
+ def cookbook(&block)
54
+ rules.last.cookbook = block
55
+ end
56
+
43
57
  # Load the ruleset
44
58
  #
45
59
  # @param [String] filename The path to the ruleset to load
@@ -15,6 +15,10 @@ module FoodCritic
15
15
  {:matched => node.respond_to?(:name) ? node.name : '', :line => pos['line'], :column => pos['column']}
16
16
  end
17
17
 
18
+ def file_match(file)
19
+ {:filename => file, :matched => file, :line => 1, :column => 1}
20
+ end
21
+
18
22
  # Does the specified recipe check for Chef Solo?
19
23
  #
20
24
  # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
@@ -20,20 +20,35 @@ module FoodCritic
20
20
  # @option options [Array] tags The tags to filter rules based on
21
21
  # @return [FoodCritic::Review] A review of your cookbooks, with any warnings issued.
22
22
  def check(cookbook_path, options)
23
- warnings = []
23
+ warnings = []; last_dir = nil
24
24
  tag_expr = Gherkin::TagExpression.new(options[:tags])
25
25
  files_to_process(cookbook_path).each do |file|
26
+ cookbook_dir = Pathname.new(File.join(File.dirname(file), '..')).cleanpath
26
27
  ast = read_file(file)
27
28
  @rules.select{|rule| tag_expr.eval(rule.tags)}.each do |rule|
28
- matches = rule.recipe.yield(ast, file)
29
- matches.each{|match| warnings << Warning.new(rule, {:filename => file}.merge(match))} unless matches.nil?
29
+ rule_matches = matches(rule.recipe, ast, file)
30
+ rule_matches += matches(rule.provider, ast, file) if File.basename(File.dirname(file)) == 'providers'
31
+ rule_matches += matches(rule.cookbook, cookbook_dir) if last_dir != cookbook_dir
32
+ rule_matches.each{|match| warnings << Warning.new(rule, {:filename => file}.merge(match))}
30
33
  end
34
+ last_dir = cookbook_dir
31
35
  end
32
36
  Review.new(warnings)
33
37
  end
34
38
 
35
39
  private
36
40
 
41
+ # Invoke the DSL method with the provided parameters.
42
+ #
43
+ # @param [Proc] match_method Proc to invoke
44
+ # @param params Parameters for the proc
45
+ # @return [Array] The returned matches
46
+ def matches(match_method, *params)
47
+ return [] unless match_method.respond_to?(:yield)
48
+ matches = match_method.yield(*params)
49
+ matches.respond_to?(:each) ? matches : []
50
+ end
51
+
37
52
  # Load the rules from the (fairly unnecessary) DSL.
38
53
  def load_rules
39
54
  @rules = RuleDsl.load(File.join(File.dirname(__FILE__), 'rules.rb'))
@@ -45,7 +60,8 @@ module FoodCritic
45
60
  # @return [Array] The files underneath the provided directory to be processed.
46
61
  def files_to_process(dir)
47
62
  return [dir] unless File.directory? dir
48
- Dir.glob(File.join(dir, '{attributes,recipes}/*.rb')) + Dir.glob(File.join(dir, '*/{attributes,recipes}/*.rb'))
63
+ Dir.glob(File.join(dir, '{attributes,providers,recipes}/*.rb')) +
64
+ Dir.glob(File.join(dir, '*/{attributes,providers,recipes}/*.rb'))
49
65
  end
50
66
 
51
67
  end
@@ -1,3 +1,13 @@
1
+ rule "FC001", "Use strings in preference to symbols to access node attributes" do
2
+ tags %w{style attributes}
3
+ description "When accessing node attributes you should use a string for a key rather than a symbol."
4
+ recipe do |ast|
5
+ %w{node default override set normal}.map do |type|
6
+ ast.xpath("//*[self::aref_field or self::aref][descendant::ident/@value='#{type}']//symbol").map{|ar| match(ar)}
7
+ end.flatten
8
+ end
9
+ end
10
+
1
11
  rule "FC002", "Avoid string interpolation where not required" do
2
12
  tags %w{style strings}
3
13
  description "When setting a resource value avoid string interpolation where not required."
@@ -21,7 +31,8 @@ rule "FC004", "Use a service resource to start and stop services" do
21
31
  recipe do |ast|
22
32
  find_resources(ast, 'execute').find_all do |cmd|
23
33
  cmd_str = (resource_attribute('command', cmd) || resource_name(cmd)).to_s
24
- cmd_str.include?('/etc/init.d') || cmd_str.start_with?('service ') || cmd_str.start_with?('/sbin/service ')
34
+ cmd_str.include?('/etc/init.d') || cmd_str.start_with?('service ') || cmd_str.start_with?('/sbin/service ') ||
35
+ cmd_str.start_with?('start ') || cmd_str.start_with?('stop ') || cmd_str.start_with?('invoke-rc.d ')
25
36
  end.map{|cmd| match(cmd)}
26
37
  end
27
38
  end
@@ -34,7 +45,7 @@ rule "FC005", "Avoid repetition of resource declarations" do
34
45
  # do all of the attributes for all resources of a given type match apart aside from one?
35
46
  resource_attributes_by_type(ast).each do |type, resource_atts|
36
47
  sorted_atts = resource_atts.map{|atts| atts.to_a.sort{|x,y| x.first.to_s <=> y.first.to_s }}
37
- if sorted_atts.all?{|att| (att - sorted_atts.inject{|atts,a| atts & a}).length == 1}
48
+ if sorted_atts.length > 2 and sorted_atts.all?{|att| (att - sorted_atts.inject{|atts,a| atts & a}).length == 1}
38
49
  matches << match(find_resources(ast, type).first)
39
50
  end
40
51
  end
@@ -68,8 +79,8 @@ end
68
79
  rule "FC008", "Generated cookbook metadata needs updating" do
69
80
  tags %w{style metadata}
70
81
  description "The cookbook metadata for this cookbook is boilerplate output from knife generate cookbook and needs updating with the real details of your cookbook."
71
- recipe do |ast,filename|
72
- metadata_path = Pathname.new(File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
82
+ cookbook do |filename|
83
+ metadata_path = Pathname.new(File.join(filename, 'metadata.rb')).cleanpath
73
84
  next unless File.exists? metadata_path
74
85
  md = read_file(metadata_path)
75
86
  {'maintainer' => 'YOUR_COMPANY_NAME', 'maintainer_email' => 'YOUR_EMAIL'}.map do |field,value|
@@ -107,4 +118,70 @@ rule "FC010", "Invalid search syntax" do
107
118
  # This only works for literal search strings
108
119
  literal_searches(ast).reject{|search| valid_query?(search['value'])}.map{|search| match(search)}
109
120
  end
121
+ end
122
+
123
+ rule "FC011", "Missing README in markdown format" do
124
+ tags %w{style readme}
125
+ cookbook do |filename|
126
+ [file_match(File.join(filename, 'README.md'))] unless File.exists?(File.join(filename, 'README.md'))
127
+ end
128
+ end
129
+
130
+ rule "FC012", "Use Markdown for README rather than RDoc" do
131
+ tags %w{style readme}
132
+ cookbook do |filename|
133
+ [file_match(File.join(filename, 'README.rdoc'))] if File.exists?(File.join(filename, 'README.rdoc'))
134
+ end
135
+ end
136
+
137
+ rule "FC013", "Use file_cache_path rather than hard-coding tmp paths" do
138
+ tags %w{style files}
139
+ recipe do |ast|
140
+ find_resources(ast, 'remote_file').find_all do |download|
141
+ path = (resource_attribute('path', download) || resource_name(download)).to_s
142
+ path.start_with?('/tmp/')
143
+ end.map{|download| match(download)}
144
+ end
145
+ end
146
+
147
+ rule "FC014", "Consider extracting long ruby_block to library" do
148
+ tags %w{style libraries}
149
+ recipe do |ast|
150
+ find_resources(ast, 'ruby_block').find_all do |rb|
151
+ ! rb.xpath("//fcall[ident/@value='block' and count(ancestor::*) = 8]/../../do_block[count(descendant::*) > 100]").empty?
152
+ end.map{|block| match(block)}
153
+ end
154
+ end
155
+
156
+ rule "FC015", "Consider converting definition to a LWRP" do
157
+ tags %w{style definitions lwrp}
158
+ cookbook do |dir|
159
+ Dir[File.join(dir, 'definitions', '*.rb')].reject{|entry| ['.', '..'].include? entry}.map{|entry| file_match(entry)}
160
+ end
161
+ end
162
+
163
+ rule "FC016", "LWRP does not declare a default action" do
164
+ tags %w{correctness lwrp}
165
+ provider do |ast, filename|
166
+ ast.xpath("//def/bodystmt/descendant::assign/var_field/ivar/@value='@action'") ? [] : [file_match(filename)]
167
+ end
168
+ end
169
+
170
+ rule "FC017", "LWRP does not notify when updated" do
171
+ tags %w{correctness lwrp}
172
+ provider do |ast, filename|
173
+ if ast.xpath(%q{//call/*[self::vcall or self::var_ref/ident/@value='new_resource']/../
174
+ ident[@value='updated_by_last_action']}).empty?
175
+ [file_match(filename)]
176
+ end
177
+ end
178
+ end
179
+
180
+ rule "FC018", "LWRP uses deprecated notification syntax" do
181
+ tags %w{style lwrp deprecated}
182
+ provider do |ast|
183
+ ast.xpath("//assign/var_field/ivar[@value='@updated']").map{|class_var| match(class_var)} +
184
+ ast.xpath(%q{//assign/field/*[self::vcall or self::var_ref/ident/@value='new_resource']/../
185
+ ident[@value='updated']}).map{|assign| match(assign)}
186
+ end
110
187
  end
@@ -1,3 +1,3 @@
1
1
  module FoodCritic
2
- VERSION = '0.5.2'
2
+ VERSION = '0.6.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.5.2
4
+ version: 0.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-15 00:00:00.000000000Z
12
+ date: 2011-12-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: chef
16
- requirement: &2155159160 !ruby/object:Gem::Requirement
16
+ requirement: &2152976100 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.10.4
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2155159160
24
+ version_requirements: *2152976100
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: json
27
- requirement: &2155158600 !ruby/object:Gem::Requirement
27
+ requirement: &2152975580 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -35,10 +35,10 @@ dependencies:
35
35
  version: 1.6.1
36
36
  type: :runtime
37
37
  prerelease: false
38
- version_requirements: *2155158600
38
+ version_requirements: *2152975580
39
39
  - !ruby/object:Gem::Dependency
40
40
  name: gherkin
41
- requirement: &2155157740 !ruby/object:Gem::Requirement
41
+ requirement: &2152974820 !ruby/object:Gem::Requirement
42
42
  none: false
43
43
  requirements:
44
44
  - - ~>
@@ -46,10 +46,10 @@ dependencies:
46
46
  version: 2.7.1
47
47
  type: :runtime
48
48
  prerelease: false
49
- version_requirements: *2155157740
49
+ version_requirements: *2152974820
50
50
  - !ruby/object:Gem::Dependency
51
51
  name: nokogiri
52
- requirement: &2155157240 !ruby/object:Gem::Requirement
52
+ requirement: &2152974340 !ruby/object:Gem::Requirement
53
53
  none: false
54
54
  requirements:
55
55
  - - ~>
@@ -57,7 +57,7 @@ dependencies:
57
57
  version: 1.5.0
58
58
  type: :runtime
59
59
  prerelease: false
60
- version_requirements: *2155157240
60
+ version_requirements: *2152974340
61
61
  description: Lint tool for Opscode Chef cookbooks.
62
62
  email:
63
63
  executables:
@@ -94,12 +94,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
94
  version: '0'
95
95
  segments:
96
96
  - 0
97
- hash: 2219223099964233335
97
+ hash: 2590602930680125427
98
98
  requirements: []
99
99
  rubyforge_project:
100
100
  rubygems_version: 1.8.10
101
101
  signing_key:
102
102
  specification_version: 3
103
- summary: foodcritic-0.5.2
103
+ summary: foodcritic-0.6.0
104
104
  test_files: []
105
105
  has_rdoc: