foodcritic 15.1.0 → 16.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/chef_dsl_metadata/{chef_14.8.12.json → chef_14.14.29.json} +394 -169
  4. data/chef_dsl_metadata/{chef_13.12.3.json → chef_15.4.45.json} +15454 -2631
  5. data/foodcritic.gemspec +2 -2
  6. data/lib/foodcritic.rb +2 -1
  7. data/lib/foodcritic/api.rb +55 -42
  8. data/lib/foodcritic/chef.rb +5 -3
  9. data/lib/foodcritic/command_line.rb +78 -52
  10. data/lib/foodcritic/domain.rb +7 -9
  11. data/lib/foodcritic/dsl.rb +4 -1
  12. data/lib/foodcritic/gerkin/tag.rb +55 -0
  13. data/lib/foodcritic/gerkin/tag_expression.rb +66 -0
  14. data/lib/foodcritic/linter.rb +24 -8
  15. data/lib/foodcritic/notifications.rb +3 -1
  16. data/lib/foodcritic/output.rb +1 -1
  17. data/lib/foodcritic/rules/fc001.rb +6 -6
  18. data/lib/foodcritic/rules/fc004.rb +1 -0
  19. data/lib/foodcritic/rules/fc006.rb +9 -9
  20. data/lib/foodcritic/rules/fc007.rb +3 -1
  21. data/lib/foodcritic/rules/fc016.rb +1 -0
  22. data/lib/foodcritic/rules/fc019.rb +7 -6
  23. data/lib/foodcritic/rules/fc022.rb +24 -25
  24. data/lib/foodcritic/rules/fc024.rb +16 -13
  25. data/lib/foodcritic/rules/fc029.rb +1 -0
  26. data/lib/foodcritic/rules/fc031.rb +1 -1
  27. data/lib/foodcritic/rules/fc032.rb +2 -2
  28. data/lib/foodcritic/rules/fc033.rb +2 -2
  29. data/lib/foodcritic/rules/fc034.rb +5 -2
  30. data/lib/foodcritic/rules/fc039.rb +12 -12
  31. data/lib/foodcritic/rules/fc040.rb +1 -1
  32. data/lib/foodcritic/rules/fc044.rb +8 -12
  33. data/lib/foodcritic/rules/fc048.rb +1 -0
  34. data/lib/foodcritic/rules/fc121.rb +6 -10
  35. data/lib/foodcritic/rules/fc123.rb +16 -0
  36. data/lib/foodcritic/template.rb +3 -6
  37. data/lib/foodcritic/version.rb +1 -1
  38. metadata +34 -32
@@ -1,5 +1,3 @@
1
- require "cucumber/core/gherkin/tag_expression"
2
-
3
1
  module FoodCritic
4
2
  # A warning of a possible issue
5
3
  class Warning
@@ -34,7 +32,7 @@ module FoodCritic
34
32
 
35
33
  def to_s
36
34
  @rules.sort_by(&:code)
37
- .map { |r| r.to_s }.join("\n")
35
+ .map(&:to_s).join("\n")
38
36
  end
39
37
 
40
38
  end
@@ -50,12 +48,12 @@ module FoodCritic
50
48
 
51
49
  # If any of the warnings in this review have failed or not.
52
50
  def failed?
53
- warnings.any? { |w| w.failed? }
51
+ warnings.any?(&:failed?)
54
52
  end
55
53
 
56
54
  # Returns an array of warnings that are marked as failed.
57
55
  def failures
58
- warnings.select { |w| w.failed? }
56
+ warnings.select(&:failed?)
59
57
  end
60
58
 
61
59
  # Returns a string representation of this review. This representation is
@@ -76,8 +74,8 @@ module FoodCritic
76
74
  # A rule to be matched against.
77
75
  class Rule
78
76
  attr_accessor :code, :name, :applies_to, :cookbook, :attributes, :recipe,
79
- :provider, :resource, :metadata, :library, :template, :role,
80
- :environment
77
+ :provider, :resource, :metadata, :library, :template, :role,
78
+ :environment
81
79
 
82
80
  attr_writer :tags
83
81
 
@@ -95,8 +93,8 @@ module FoodCritic
95
93
 
96
94
  # Checks the rule tags to see if they match a Gherkin (Cucumber) expression
97
95
  def matches_tags?(tag_expr)
98
- Cucumber::Core::Gherkin::TagExpression.new(tag_expr).evaluate(
99
- tags.map { |tag| Cucumber::Core::Ast::Tag.new(nil, tag) }
96
+ ::Foodcritic::Gherkin::TagExpression.new(tag_expr).evaluate(
97
+ tags.map { |tag| ::Foodcritic::Gherkin::Tag.new(nil, tag) }
100
98
  )
101
99
  end
102
100
 
@@ -1,3 +1,4 @@
1
+ require "open-uri"
1
2
  require "pathname"
2
3
 
3
4
  module FoodCritic
@@ -76,7 +77,9 @@ module FoodCritic
76
77
  paths.map do |path|
77
78
  File.directory?(path) ? Dir["#{path}/**/*.rb"].sort : path
78
79
  end.flatten.each do |path|
79
- dsl.instance_eval(File.read(path), path)
80
+ rule_file = open(path)
81
+ dsl.instance_eval(rule_file.read, path)
82
+ rule_file.close
80
83
  end
81
84
  dsl.rules
82
85
  end
@@ -0,0 +1,55 @@
1
+ # this is vendored from cucumber-core gem with the module/class names changed
2
+
3
+ module Foodcritic
4
+ module Gherkin
5
+ class Tag
6
+
7
+ attr_reader :name
8
+
9
+ def initialize(location, name)
10
+ @location = location
11
+ @name = name
12
+ end
13
+
14
+ def inspect
15
+ %{#<#{self.class} "#{name}" (#{location})>}
16
+ end
17
+
18
+ def file_colon_line
19
+ location.to_s
20
+ end
21
+
22
+ def file
23
+ location.file
24
+ end
25
+
26
+ def line
27
+ location.line
28
+ end
29
+
30
+ def location
31
+ raise('Please set @location in the constructor') unless defined?(@location)
32
+ @location
33
+ end
34
+
35
+ def attributes
36
+ [tags, comments, multiline_arg].flatten
37
+ end
38
+
39
+ def tags
40
+ # will be overriden by nodes that actually have tags
41
+ []
42
+ end
43
+
44
+ def comments
45
+ # will be overriden by nodes that actually have comments
46
+ []
47
+ end
48
+
49
+ def multiline_arg
50
+ # will be overriden by nodes that actually have a multiline_argument
51
+ Test::EmptyMultilineArgument.new
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,66 @@
1
+ # this is vendored from cucumber-core gem with the module/class names changed
2
+ # in the 4.0 release of cucumber-core they removed support for legacy tags (~FC100)
3
+ # and we're not going to force everyone to redo their tags on a EOL product
4
+
5
+ module Foodcritic
6
+ module Gherkin
7
+ class TagExpression
8
+
9
+ attr_reader :limits
10
+
11
+ def initialize(tag_expressions)
12
+ @ands = []
13
+ @limits = {}
14
+ tag_expressions.each do |expr|
15
+ add(expr.strip.split(/\s*,\s*/))
16
+ end
17
+ end
18
+
19
+ def empty?
20
+ @ands.empty?
21
+ end
22
+
23
+ def evaluate(tags)
24
+ return true if @ands.flatten.empty?
25
+ vars = Hash[*tags.map{|tag| [tag.name, true]}.flatten]
26
+ raise "No vars" if vars.nil? # Useless statement to prevent ruby warnings about unused var
27
+ !!Kernel.eval(ruby_expression)
28
+ end
29
+
30
+ private
31
+
32
+ def add(tags_with_negation_and_limits)
33
+ negatives, positives = tags_with_negation_and_limits.partition{|tag| tag =~ /^~/}
34
+ @ands << (store_and_extract_limits(negatives, true) + store_and_extract_limits(positives, false))
35
+ end
36
+
37
+ def store_and_extract_limits(tags_with_negation_and_limits, negated)
38
+ tags_with_negation = []
39
+ tags_with_negation_and_limits.each do |tag_with_negation_and_limit|
40
+ tag_with_negation, limit = tag_with_negation_and_limit.split(':')
41
+ tags_with_negation << tag_with_negation
42
+ if limit
43
+ tag_without_negation = negated ? tag_with_negation[1..-1] : tag_with_negation
44
+ if @limits[tag_without_negation] && @limits[tag_without_negation] != limit.to_i
45
+ raise "Inconsistent tag limits for #{tag_without_negation}: #{@limits[tag_without_negation]} and #{limit.to_i}"
46
+ end
47
+ @limits[tag_without_negation] = limit.to_i
48
+ end
49
+ end
50
+ tags_with_negation
51
+ end
52
+
53
+ def ruby_expression
54
+ "(" + @ands.map do |ors|
55
+ ors.map do |tag|
56
+ if tag =~ /^~(.*)/
57
+ "!vars['#{$1}']"
58
+ else
59
+ "vars['#{tag}']"
60
+ end
61
+ end.join("||")
62
+ end.join(")&&(") + ")"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,3 +1,4 @@
1
+ require "open-uri"
1
2
  require "optparse"
2
3
  require "ripper"
3
4
  require "set"
@@ -9,7 +10,7 @@ module FoodCritic
9
10
 
10
11
  # The default version that will be used to determine relevant rules. This
11
12
  # can be over-ridden at the command line with the `--chef-version` option.
12
- DEFAULT_CHEF_VERSION = "14.8.12".freeze
13
+ DEFAULT_CHEF_VERSION = "15.4.45".freeze
13
14
  attr_reader :chef_version
14
15
 
15
16
  # Perform a lint check. This method is intended for use by the command-line
@@ -19,6 +20,7 @@ module FoodCritic
19
20
  # The first item is the string output, the second is exit code.
20
21
  return [cmd_line.help, 0] if cmd_line.show_help?
21
22
  return [cmd_line.version, 0] if cmd_line.show_version?
23
+
22
24
  if !cmd_line.valid_grammar?
23
25
  [cmd_line.help, 4]
24
26
  elsif cmd_line.list_rules?
@@ -68,6 +70,7 @@ module FoodCritic
68
70
  options = setup_defaults(options)
69
71
  @options = options
70
72
  @chef_version = options[:chef_version] || DEFAULT_CHEF_VERSION
73
+ ast_cache(options[:ast_cache_size])
71
74
 
72
75
  warnings = []; last_dir = nil; matched_rule_tags = Set.new
73
76
  load_rules
@@ -111,8 +114,8 @@ module FoodCritic
111
114
  # Convert the matches into warnings
112
115
  matches.each do |match|
113
116
  warnings << Warning.new(state[:rule],
114
- { filename: state[:file] }.merge(match),
115
- options)
117
+ { filename: state[:file] }.merge(match),
118
+ options)
116
119
  matched_rule_tags << state[:rule].tags
117
120
  end
118
121
  end
@@ -132,16 +135,19 @@ module FoodCritic
132
135
 
133
136
  if dsl_method_for_file(state[:file])
134
137
  cbk_matches += matches(state[:rule].send(
135
- dsl_method_for_file(state[:file])), state[:ast], state[:file])
138
+ dsl_method_for_file(state[:file])
139
+ ), state[:ast], state[:file])
136
140
  end
137
141
 
138
142
  per_cookbook_rules(state[:last_dir], state[:file]) do
139
143
  if File.basename(state[:file]) == "metadata.rb"
140
144
  cbk_matches += matches(
141
- state[:rule].metadata, state[:ast], state[:file])
145
+ state[:rule].metadata, state[:ast], state[:file]
146
+ )
142
147
  end
143
148
  cbk_matches += matches(
144
- state[:rule].cookbook, cookbook_dir(state[:file]))
149
+ state[:rule].cookbook, cookbook_dir(state[:file])
150
+ )
145
151
  end
146
152
 
147
153
  cbk_matches
@@ -196,6 +202,7 @@ module FoodCritic
196
202
  # Some rules are version specific.
197
203
  def applies_to_version?(rule, version)
198
204
  return true unless version
205
+
199
206
  rule.applies_to.yield(Gem::Version.create(version))
200
207
  end
201
208
 
@@ -216,7 +223,8 @@ module FoodCritic
216
223
 
217
224
  # if a rule file has been specified use that. Otherwise use the .foodcritic file in the CB
218
225
  tags = if @options[:rule_file]
219
- raise "ERROR: Could not find the specified rule file at #{@options[:rule_file]}" unless File.exist?(@options[:rule_file])
226
+ raise "ERROR: Could not find the specified rule file at #{@options[:rule_file]}" if is_local_file?(@options[:rule_file]) && ! File.exist?(@options[:rule_file])
227
+
220
228
  parse_rule_file(@options[:rule_file])
221
229
  else
222
230
  File.exist?("#{cookbook}/.foodcritic") ? parse_rule_file("#{cookbook}/.foodcritic") : []
@@ -233,7 +241,9 @@ module FoodCritic
233
241
  def parse_rule_file(file)
234
242
  tags = []
235
243
  begin
236
- tag_text = File.read file
244
+ rule_file = open(file)
245
+ tag_text = rule_file.read
246
+ rule_file.close
237
247
  tags = tag_text.split(/\s/)
238
248
  rescue
239
249
  raise "ERROR: Could not read or parse the specified rule file at #{file}"
@@ -338,9 +348,15 @@ module FoodCritic
338
348
  end.flatten
339
349
  end
340
350
 
351
+ # Determine if the rule file name is a local file or a url
352
+ def is_local_file?(file_name)
353
+ ! /^(ftps*:|https*:|file:)/.match(file_name)
354
+ end
355
+
341
356
  # Invoke the DSL method with the provided parameters.
342
357
  def matches(match_method, *params)
343
358
  return [] unless match_method.respond_to?(:yield)
359
+
344
360
  matches = match_method.yield(*params)
345
361
  return [] unless matches.respond_to?(:each)
346
362
 
@@ -133,13 +133,15 @@ module FoodCritic
133
133
  is_variable = true unless notify.xpath("args_add_block/args_add//args_add[aref or vcall or call or var_ref]").empty?
134
134
  string_val = notify.xpath("descendant::args_add/string_literal/string_add/tstring_content/@value").first
135
135
  symbol_val = notify.xpath('descendant::args_add/args_add//symbol/ident/@value |
136
- descendant::dyna_symbol[1]/xstring_add/tstring_content/@value').first
136
+ descendant::dyna_symbol[1]/xstring_add/tstring_content/@value |
137
+ descendant::args_add/args_add//dyna_symbol/string_add/tstring_content/@value').first
137
138
 
138
139
  # 1) return a nil if the action is a variable like node['foo']['bar']
139
140
  # 2) return the symbol if it exists
140
141
  # 3) return the string since we're positive that we're not a symbol or variable
141
142
  return nil if is_variable
142
143
  return symbol_val.value.to_sym unless symbol_val.nil?
144
+
143
145
  string_val.value
144
146
  end
145
147
 
@@ -128,7 +128,7 @@ module FoodCritic
128
128
  fmt << 40 + colors.index(bg.to_s) if bg
129
129
  fmt << attrs.index(attr.to_s) if attr
130
130
  if fmt
131
- puts "#{escape % fmt.join(';')}#{text}#{escape % 0}"
131
+ puts "#{escape % fmt.join(";")}#{text}#{escape % 0}"
132
132
  else
133
133
  puts text
134
134
  end
@@ -1,8 +1,8 @@
1
1
  rule "FC001",
2
- "Use strings in preference to symbols to access node attributes" do
3
- tags %w{style attributes}
4
- recipe do |ast|
5
- # node.run_state is not actually an attribute so ignore that
6
- attribute_access(ast, type: :symbol, ignore: "run_state")
2
+ "Use strings in preference to symbols to access node attributes" do
3
+ tags %w{style attributes}
4
+ recipe do |ast|
5
+ # node.run_state is not actually an attribute so ignore that
6
+ attribute_access(ast, type: :symbol, ignore: "run_state")
7
+ end
7
8
  end
8
- end
@@ -4,6 +4,7 @@ rule "FC004", "Use a service resource to start and stop services" do
4
4
  find_resources(ast, type: "execute").find_all do |cmd|
5
5
  cmd_str = (resource_attribute(cmd, "command") || resource_name(cmd)).to_s
6
6
  next if cmd_str.include?(".exe") # don't catch windows commands
7
+
7
8
  cmd_str.start_with?( "start ", "stop ", "reload ", "restart ") || # upstart jobs
8
9
  ( [ "/etc/init.d", "service ", "/sbin/service ", "invoke-rc.d ", "systemctl "].any? do |service_cmd| # upstart / sys-v / systemd
9
10
  cmd_str.start_with?(service_cmd)
@@ -1,11 +1,11 @@
1
1
  rule "FC006", "Mode should be quoted or fully specified when "\
2
2
  "setting file permissions" do
3
- tags %w{correctness files}
4
- recipe do |ast|
5
- ast.xpath(%q{//ident[@value='mode']/parent::command/
6
- descendant::int[string-length(@value) < 5
7
- and not(starts-with(@value, "0")
8
- and string-length(@value) = 4)][count(ancestor::aref) = 0]/
9
- ancestor::method_add_block})
10
- end
11
- end
3
+ tags %w{correctness files}
4
+ recipe do |ast|
5
+ ast.xpath(%q{//ident[@value='mode']/parent::command/
6
+ descendant::int[string-length(@value) < 5
7
+ and not(starts-with(@value, "0")
8
+ and string-length(@value) = 4)][count(ancestor::aref) = 0]/
9
+ ancestor::method_add_block})
10
+ end
11
+ end
@@ -2,8 +2,10 @@ rule "FC007", "Ensure recipe dependencies are reflected in cookbook metadata" do
2
2
  tags %w{correctness metadata}
3
3
  recipe do |ast, filename|
4
4
  metadata_path = Pathname.new(
5
- File.join(File.dirname(filename), "..", "metadata.rb")).cleanpath
5
+ File.join(File.dirname(filename), "..", "metadata.rb")
6
+ ).cleanpath
6
7
  next unless File.exist? metadata_path
8
+
7
9
  actual_included = included_recipes(ast, with_partial_names: false)
8
10
  undeclared = actual_included.keys.map do |recipe|
9
11
  recipe.split("::").first unless recipe =~ /^::/ # skip shorthand included recipes. They're local
@@ -3,6 +3,7 @@ rule "FC016", "LWRP does not declare a default action" do
3
3
  resource do |ast, filename|
4
4
  # See if we're in a custom resource not an LWRP. Only LWRPs need the default_action
5
5
  next if ast.xpath("//ident/@value='action'")
6
+
6
7
  unless ["//ident/@value='default_action'",
7
8
  "//def/bodystmt/descendant::assign/
8
9
  var_field/ivar/@value='@action'"].any? { |expr| ast.xpath(expr) }
@@ -3,20 +3,21 @@ rule "FC019", "Access node attributes in a consistent manner" do
3
3
  cookbook do |cookbook_dir|
4
4
  asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].reject do |file|
5
5
  relative_path = Pathname.new(file).relative_path_from(
6
- Pathname.new(cookbook_dir))
6
+ Pathname.new(cookbook_dir)
7
+ )
7
8
  relative_path.to_s.split(File::SEPARATOR).include?("spec")
8
9
  end.map do |file|
9
10
  { path: file, ast: read_ast(file) }
10
11
  end
11
- types = [:string, :symbol, :vivified].map do |type|
12
+ types = %i{string symbol vivified}.map do |type|
12
13
  {
13
14
  access_type: type, count: files.map do |file|
14
15
  attribute_access(file[:ast], type: type, ignore_calls: true,
15
16
  cookbook_dir: cookbook_dir, ignore: "run_state").tap do |ast|
16
- unless ast.empty?
17
- (asts[type] ||= []) << { ast: ast, path: file[:path] }
18
- end
19
- end.size
17
+ unless ast.empty?
18
+ (asts[type] ||= []) << { ast: ast, path: file[:path] }
19
+ end
20
+ end.size
20
21
  end.inject(:+)
21
22
  }
22
23
  end.reject { |type| type[:count] == 0 }
@@ -4,30 +4,29 @@ rule "FC022", "Resource condition within loop may not behave as expected" do
4
4
  ast.xpath("//call[ident/@value='each']/../do_block[count(ancestor::
5
5
  method_add_block/method_add_arg/fcall/ident[@value='only_if' or
6
6
  @value = 'not_if']) = 0]").map do |lp|
7
- block_vars = lp.xpath("block_var/params/child::*").map do |n|
8
- n.name.sub(/^ident/, "")
9
- end + lp.xpath("block_var/params/child::*/descendant::ident").map do |v|
10
- v["value"]
11
- end
12
- find_resources(lp).map do |resource|
13
- # if any of the parameters to the block are used in a condition then we
14
- # have a match
15
- unless (block_vars &
16
- (resource.xpath(%q{descendant::ident[@value='not_if' or
17
- @value='only_if']/ancestor::*[self::method_add_block or
18
- self::command][1]/descendant::ident/@value}).map do |a|
19
- a.value
20
- end)).empty?
21
- c = resource.xpath("command[count(descendant::string_embexpr) = 0]")
22
- if resource.xpath("command/ident/@value").first.value == "define"
23
- next
24
- end
25
- resource unless c.empty? || block_vars.any? do |var|
26
- !resource.xpath(%Q{command/args_add_block/args_add/
27
- var_ref/ident[@value='#{var}']}).empty?
28
- end
29
- end
30
- end
31
- end.flatten.compact
7
+ block_vars = lp.xpath("block_var/params/child::*").map do |n|
8
+ n.name.sub(/^ident/, "")
9
+ end + lp.xpath("block_var/params/child::*/descendant::ident").map do |v|
10
+ v["value"]
11
+ end
12
+ find_resources(lp).map do |resource|
13
+ # if any of the parameters to the block are used in a condition then we
14
+ # have a match
15
+ unless (block_vars &
16
+ (resource.xpath(%q{descendant::ident[@value='not_if' or
17
+ @value='only_if']/ancestor::*[self::method_add_block or
18
+ self::command][1]/descendant::ident/@value}).map(&:value))).empty?
19
+ c = resource.xpath("command[count(descendant::string_embexpr) = 0]")
20
+ if resource.xpath("command/ident/@value").first.value == "define"
21
+ next
22
+ end
23
+
24
+ resource unless c.empty? || block_vars.any? do |var|
25
+ !resource.xpath(%Q{command/args_add_block/args_add/
26
+ var_ref/ident[@value='#{var}']}).empty?
27
+ end
28
+ end
29
+ end
30
+ end.flatten.compact
32
31
  end
33
32
  end