foodcritic 0.6.0 → 0.7.0

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