foodcritic 0.6.0 → 0.7.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/bin/foodcritic CHANGED
@@ -1,19 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'foodcritic'
3
- require 'optparse'
4
-
5
- options = {}
6
- options[:tags] = []
7
- parser = OptionParser.new do |opts|
8
- opts.banner = 'foodcritic [cookbook_path]'
9
- opts.on("-t", "--tags TAGS", "Only check against rules with the specified tags.") {|t|options[:tags] << t}
10
- end
11
- parser.parse!
12
-
13
- unless ARGV.length == 1 and Dir.exists?(ARGV[0])
14
- puts parser.help
15
- exit 1
16
- end
17
-
18
- review = FoodCritic::Linter.new.check(ARGV[0], options)
19
- puts review unless review.warnings.empty?
3
+ result, status = FoodCritic::Linter.check(ARGV)
4
+ puts result; exit status.to_i
data/lib/foodcritic.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'chef'
2
+ require 'pry'
2
3
  require 'foodcritic/domain'
3
4
  require 'foodcritic/helpers'
4
5
  require 'foodcritic/dsl'
@@ -24,8 +24,17 @@ module FoodCritic
24
24
  # Create a new review
25
25
  #
26
26
  # @param [Array] warnings The warnings raised in this review
27
- def initialize(warnings)
27
+ # @param [Boolean] is_failed Have warnings been raised that mean this should be considered failed?
28
+ def initialize(warnings, is_failed)
28
29
  @warnings = warnings
30
+ @is_failed = is_failed
31
+ end
32
+
33
+ # If this review has failed or not.
34
+ #
35
+ # @return [Boolean] True if this review has failed.
36
+ def failed?
37
+ @is_failed
29
38
  end
30
39
 
31
40
  # Returns a string representation of this review.
@@ -40,7 +49,7 @@ module FoodCritic
40
49
 
41
50
  # A rule to be matched against.
42
51
  class Rule
43
- attr_accessor :code, :name, :description, :cookbook, :recipe, :provider, :tags
52
+ attr_accessor :code, :name, :cookbook, :recipe, :provider, :tags
44
53
 
45
54
  # Create a new rule
46
55
  #
@@ -50,6 +59,13 @@ module FoodCritic
50
59
  @code, @name = code, name
51
60
  @tags = [code]
52
61
  end
62
+
63
+ # Returns a string representation of this rule.
64
+ #
65
+ # @return [String] Rule as a string.
66
+ def to_s
67
+ "#{@code}: #{@name}"
68
+ end
53
69
  end
54
70
 
55
71
  end
@@ -1,5 +1,6 @@
1
1
  require 'pathname'
2
2
 
3
+ # FoodCritic is a lint tool for Chef cookbooks.
3
4
  module FoodCritic
4
5
 
5
6
  # The DSL methods exposed for defining rules.
@@ -26,13 +27,6 @@ module FoodCritic
26
27
  rules.last.tags += tags
27
28
  end
28
29
 
29
- # Set the rule description
30
- #
31
- # @param [String] description Set the rule description.
32
- def description(description)
33
- rules.last.description = description
34
- end
35
-
36
30
  # Define a matcher that will be passed the AST with this method.
37
31
  #
38
32
  # @param [block] block Your implemented matcher that returns a match Hash.
@@ -58,9 +52,10 @@ module FoodCritic
58
52
  #
59
53
  # @param [String] filename The path to the ruleset to load
60
54
  # @return [Array] The loaded rules, ready to be matched against provided cookbooks.
61
- def self.load(filename)
55
+ def self.load(filename, with_repl)
62
56
  dsl = RuleDsl.new
63
57
  dsl.instance_eval(File.read(filename), filename)
58
+ dsl.instance_eval { binding.pry } if with_repl
64
59
  dsl.rules
65
60
  end
66
61
  end
@@ -15,6 +15,11 @@ module FoodCritic
15
15
  {:matched => node.respond_to?(:name) ? node.name : '', :line => pos['line'], :column => pos['column']}
16
16
  end
17
17
 
18
+ # Create a match for a specified file. Use this if the presence of the file triggers the warning rather than content.
19
+ #
20
+ # @param [String] file The filename to create a match for
21
+ # @return [Hash] Hash with the match details
22
+ # @see FoodCritic::Helpers#match
18
23
  def file_match(file)
19
24
  {:filename => file, :matched => file, :line => 1, :column => 1}
20
25
  end
@@ -28,6 +33,18 @@ module FoodCritic
28
33
  count(descendant::ident[@value='solo']) > 0]}).empty?
29
34
  end
30
35
 
36
+ # Is the chef-solo-search library available?
37
+ #
38
+ # @param [String] recipe_path The path to the current recipe
39
+ # @return [Boolean] True if the chef-solo-search library is available.
40
+ def chef_solo_search_supported?(recipe_path)
41
+ search_libs = Dir[File.join(Pathname.new(File.join(recipe_path, '../../..')).realpath, "**/libraries/search.rb")]
42
+ search_libs.any? do |lib|
43
+ ! read_file(lib).xpath(%q{//class[count(descendant::const[@value='Chef' or @value='Recipe']) = 2]/
44
+ descendant::def/ident[@value='search']}).empty?
45
+ end
46
+ end
47
+
31
48
  # Searches performed by the specified recipe.
32
49
  #
33
50
  # @param [Nokogiri::XML::Node] ast The AST of the cookbook recipe to check.
@@ -173,7 +190,7 @@ module FoodCritic
173
190
 
174
191
  # Recurse the nested arrays provided by Ripper to create a tree we can more easily apply expressions to.
175
192
  #
176
- # @param [Hash] node The AST
193
+ # @param [Array] node The AST
177
194
  # @param [Nokogiri::XML::Document] doc The document being constructed
178
195
  # @param [Nokogiri::XML::Node] xml_node The current node
179
196
  # @return [Nokogiri::XML::Node] The XML representation
@@ -1,3 +1,4 @@
1
+ require 'optparse'
1
2
  require 'ripper'
2
3
  require 'gherkin/tag_expression'
3
4
 
@@ -8,9 +9,35 @@ module FoodCritic
8
9
 
9
10
  include FoodCritic::Helpers
10
11
 
11
- # Create a new Linter, loading any defined rules.
12
+ # Perform option parsing from the provided arguments and do a lint check based on those arguments.
13
+ #
14
+ # @param [Array] args The command-line arguments to parse
15
+ # @return [Array] Pair - the first item is string output, the second is the exit code.
16
+ def self.check(args)
17
+ options = {}
18
+ options[:fail_tags] = []; options[:tags] = []
19
+ parser = OptionParser.new do |opts|
20
+ opts.banner = 'foodcritic [cookbook_path]'
21
+ opts.on("-r", "--[no-]repl", "Drop into a REPL for interactive rule editing.") {|r|options[:repl] = r}
22
+ opts.on("-t", "--tags TAGS", "Only check against rules with the specified tags.") {|t|options[:tags] << t}
23
+ opts.on("-f", "--epic-fail TAGS", "Fail the build if any of the specified tags are matched.") {|t|options[:fail_tags] << t}
24
+ end
25
+
26
+ return [parser.help, 0] if args.length == 1 and args.first == '--help'
27
+
28
+ parser.parse!(args)
29
+
30
+ if args.length == 1 and Dir.exists?(args[0])
31
+ review = FoodCritic::Linter.new.check(args[0], options)
32
+ [review, review.failed? ? 3 : 0]
33
+ else
34
+ [parser.help, 2]
35
+ end
36
+ end
37
+
38
+ # Create a new Linter.
12
39
  def initialize
13
- load_rules
40
+
14
41
  end
15
42
 
16
43
  # Review the cookbooks at the provided path, identifying potential improvements.
@@ -18,10 +45,14 @@ module FoodCritic
18
45
  # @param [String] cookbook_path The file path to an individual cookbook directory
19
46
  # @param [Hash] options Options to apply to the linting
20
47
  # @option options [Array] tags The tags to filter rules based on
48
+ # @option options [Array] fail_tags The tags to fail the build on
21
49
  # @return [FoodCritic::Review] A review of your cookbooks, with any warnings issued.
22
50
  def check(cookbook_path, options)
23
- warnings = []; last_dir = nil
51
+ @last_cookbook_path, @last_options = cookbook_path, options
52
+ load_rules unless defined? @rules
53
+ warnings = []; last_dir = nil; matched_rule_tags = Set.new
24
54
  tag_expr = Gherkin::TagExpression.new(options[:tags])
55
+
25
56
  files_to_process(cookbook_path).each do |file|
26
57
  cookbook_dir = Pathname.new(File.join(File.dirname(file), '..')).cleanpath
27
58
  ast = read_file(file)
@@ -29,11 +60,37 @@ module FoodCritic
29
60
  rule_matches = matches(rule.recipe, ast, file)
30
61
  rule_matches += matches(rule.provider, ast, file) if File.basename(File.dirname(file)) == 'providers'
31
62
  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))}
63
+ rule_matches.each do |match|
64
+ warnings << Warning.new(rule, {:filename => file}.merge(match))
65
+ matched_rule_tags += rule.tags
66
+ end
33
67
  end
34
68
  last_dir = cookbook_dir
35
69
  end
36
- Review.new(warnings)
70
+
71
+ @review = Review.new(warnings, should_fail_build?(options[:fail_tags], matched_rule_tags))
72
+
73
+ binding.pry if options[:repl]
74
+ @review
75
+ end
76
+
77
+ # Convenience method to repeat the last check. Intended to be used from the REPL.
78
+ def recheck
79
+ check(@last_cookbook_path, @last_options)
80
+ end
81
+
82
+ # Load the rules from the (fairly unnecessary) DSL.
83
+ def load_rules
84
+ @rules = RuleDsl.load(File.join(File.dirname(__FILE__), 'rules.rb'), @last_options[:repl])
85
+ end
86
+
87
+ alias_method :reset_rules, :load_rules
88
+
89
+ # Convenience method to retrieve the last review. Intended to be used from the REPL.
90
+ #
91
+ # @return [Review] The last review performed.
92
+ def review
93
+ @review
37
94
  end
38
95
 
39
96
  private
@@ -49,11 +106,6 @@ module FoodCritic
49
106
  matches.respond_to?(:each) ? matches : []
50
107
  end
51
108
 
52
- # Load the rules from the (fairly unnecessary) DSL.
53
- def load_rules
54
- @rules = RuleDsl.load(File.join(File.dirname(__FILE__), 'rules.rb'))
55
- end
56
-
57
109
  # Return the files within a cookbook tree that we are interested in trying to match rules against.
58
110
  #
59
111
  # @param [String] dir The cookbook directory
@@ -64,5 +116,20 @@ module FoodCritic
64
116
  Dir.glob(File.join(dir, '*/{attributes,providers,recipes}/*.rb'))
65
117
  end
66
118
 
119
+ # Whether to fail the build.
120
+ #
121
+ # @param [Array] fail_tags The tags that should cause the build to fail, or special value 'any' for any tag.
122
+ # @param [Set] matched_tags The tags of warnings we have matches for
123
+ # @return [Boolean] True if the build should be failed
124
+ def should_fail_build?(fail_tags, matched_tags)
125
+ if fail_tags.empty?
126
+ false
127
+ elsif fail_tags.include? 'any'
128
+ true
129
+ else
130
+ Gherkin::TagExpression.new(fail_tags).eval(matched_tags)
131
+ end
132
+ end
133
+
67
134
  end
68
135
  end
@@ -1,6 +1,5 @@
1
1
  rule "FC001", "Use strings in preference to symbols to access node attributes" do
2
2
  tags %w{style attributes}
3
- description "When accessing node attributes you should use a string for a key rather than a symbol."
4
3
  recipe do |ast|
5
4
  %w{node default override set normal}.map do |type|
6
5
  ast.xpath("//*[self::aref_field or self::aref][descendant::ident/@value='#{type}']//symbol").map{|ar| match(ar)}
@@ -10,7 +9,6 @@ end
10
9
 
11
10
  rule "FC002", "Avoid string interpolation where not required" do
12
11
  tags %w{style strings}
13
- description "When setting a resource value avoid string interpolation where not required."
14
12
  recipe do |ast|
15
13
  ast.xpath(%q{//string_literal[count(descendant::string_embexpr) = 1 and
16
14
  count(string_add/tstring_content|string_add/string_add/tstring_content) = 0]}).map{|str| match(str)}
@@ -19,15 +17,13 @@ end
19
17
 
20
18
  rule "FC003", "Check whether you are running with chef server before using server-specific features" do
21
19
  tags %w{portability solo}
22
- description "Ideally your cookbooks should be usable without requiring chef server."
23
- recipe do |ast|
24
- checks_for_chef_solo?(ast) ? [] : searches(ast).map{|s| match(s)}
20
+ recipe do |ast,filename|
21
+ searches(ast).map{|s| match(s)} unless checks_for_chef_solo?(ast) or chef_solo_search_supported?(filename)
25
22
  end
26
23
  end
27
24
 
28
25
  rule "FC004", "Use a service resource to start and stop services" do
29
26
  tags %w{style services}
30
- description "Avoid use of execute to control services - use the service resource instead."
31
27
  recipe do |ast|
32
28
  find_resources(ast, 'execute').find_all do |cmd|
33
29
  cmd_str = (resource_attribute('command', cmd) || resource_name(cmd)).to_s
@@ -39,7 +35,6 @@ end
39
35
 
40
36
  rule "FC005", "Avoid repetition of resource declarations" do
41
37
  tags %w{style}
42
- description "Where you have a lot of resources that vary in only a single attribute wrap them in a loop for brevity."
43
38
  recipe do |ast|
44
39
  matches = []
45
40
  # do all of the attributes for all resources of a given type match apart aside from one?
@@ -55,7 +50,6 @@ end
55
50
 
56
51
  rule "FC006", "Mode should be quoted or fully specified when setting file permissions" do
57
52
  tags %w{correctness files}
58
- description "Not quoting mode when setting permissions can lead to incorrect permissions being set."
59
53
  recipe do |ast|
60
54
  ast.xpath(%q{//ident[@value='mode']/parent::command/descendant::int[string-length(@value) < 4]/
61
55
  ancestor::method_add_block}).map{|resource| match(resource)}
@@ -64,21 +58,19 @@ end
64
58
 
65
59
  rule "FC007", "Ensure recipe dependencies are reflected in cookbook metadata" do
66
60
  tags %w{correctness metadata}
67
- description "You are including a recipe that is not in the current cookbook and not defined as a dependency in your cookbook metadata."
68
61
  recipe do |ast,filename|
69
62
  metadata_path = Pathname.new(File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
70
63
  next unless File.exists? metadata_path
71
64
  undeclared = included_recipes(ast).keys.map{|recipe|recipe.split('::').first} - [cookbook_name(filename)] -
72
65
  declared_dependencies(read_file(metadata_path))
73
66
  included_recipes(ast).map do |recipe, resource|
74
- match(resource).merge(:filename => metadata_path) if undeclared.include?(recipe) || undeclared.any?{|u| recipe.start_with?("#{u}::")}
67
+ match(resource) if undeclared.include?(recipe) || undeclared.any?{|u| recipe.start_with?("#{u}::")}
75
68
  end.compact
76
69
  end
77
70
  end
78
71
 
79
72
  rule "FC008", "Generated cookbook metadata needs updating" do
80
73
  tags %w{style metadata}
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."
82
74
  cookbook do |filename|
83
75
  metadata_path = Pathname.new(File.join(filename, 'metadata.rb')).cleanpath
84
76
  next unless File.exists? metadata_path
@@ -93,7 +85,6 @@ end
93
85
 
94
86
  rule "FC009", "Resource attribute not recognised" do
95
87
  tags %w{correctness}
96
- description "You appear to be using an unrecognised attribute on a standard Chef resource. Please check for typos."
97
88
  recipe do |ast|
98
89
  matches = []
99
90
  resource_attributes_by_type(ast).each do |type,resources|
@@ -113,7 +104,6 @@ end
113
104
 
114
105
  rule "FC010", "Invalid search syntax" do
115
106
  tags %w{correctness search}
116
- description "The search expression in the recipe could not be parsed. Please check your syntax."
117
107
  recipe do |ast|
118
108
  # This only works for literal search strings
119
109
  literal_searches(ast).reject{|search| valid_query?(search['value'])}.map{|search| match(search)}
@@ -1,3 +1,4 @@
1
1
  module FoodCritic
2
- VERSION = '0.6.0'
2
+ # The current version of foodcritic
3
+ VERSION = '0.7.0'
3
4
  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.6.0
4
+ version: 0.7.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-18 00:00:00.000000000 Z
12
+ date: 2011-12-31 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: chef
16
- requirement: &2152976100 !ruby/object:Gem::Requirement
16
+ requirement: &2160561320 !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: *2152976100
24
+ version_requirements: *2160561320
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: json
27
- requirement: &2152975580 !ruby/object:Gem::Requirement
27
+ requirement: &2160560580 !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: *2152975580
38
+ version_requirements: *2160560580
39
39
  - !ruby/object:Gem::Dependency
40
40
  name: gherkin
41
- requirement: &2152974820 !ruby/object:Gem::Requirement
41
+ requirement: &2160559620 !ruby/object:Gem::Requirement
42
42
  none: false
43
43
  requirements:
44
44
  - - ~>
@@ -46,10 +46,21 @@ dependencies:
46
46
  version: 2.7.1
47
47
  type: :runtime
48
48
  prerelease: false
49
- version_requirements: *2152974820
49
+ version_requirements: *2160559620
50
+ - !ruby/object:Gem::Dependency
51
+ name: gist
52
+ requirement: &2160558760 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ version: 2.0.4
58
+ type: :runtime
59
+ prerelease: false
60
+ version_requirements: *2160558760
50
61
  - !ruby/object:Gem::Dependency
51
62
  name: nokogiri
52
- requirement: &2152974340 !ruby/object:Gem::Requirement
63
+ requirement: &2160557900 !ruby/object:Gem::Requirement
53
64
  none: false
54
65
  requirements:
55
66
  - - ~>
@@ -57,7 +68,29 @@ dependencies:
57
68
  version: 1.5.0
58
69
  type: :runtime
59
70
  prerelease: false
60
- version_requirements: *2152974340
71
+ version_requirements: *2160557900
72
+ - !ruby/object:Gem::Dependency
73
+ name: pry
74
+ requirement: &2160556980 !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ~>
78
+ - !ruby/object:Gem::Version
79
+ version: 0.9.7.4
80
+ type: :runtime
81
+ prerelease: false
82
+ version_requirements: *2160556980
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-doc
85
+ requirement: &2160556240 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ~>
89
+ - !ruby/object:Gem::Version
90
+ version: 0.3.0
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: *2160556240
61
94
  description: Lint tool for Opscode Chef cookbooks.
62
95
  email:
63
96
  executables:
@@ -94,12 +127,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
127
  version: '0'
95
128
  segments:
96
129
  - 0
97
- hash: 2590602930680125427
130
+ hash: -1380277540148398826
98
131
  requirements: []
99
132
  rubyforge_project:
100
133
  rubygems_version: 1.8.10
101
134
  signing_key:
102
135
  specification_version: 3
103
- summary: foodcritic-0.6.0
136
+ summary: foodcritic-0.7.0
104
137
  test_files: []
105
138
  has_rdoc: