foodcritic 1.7.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/chef_dsl_metadata.json +557 -0
- data/lib/foodcritic.rb +0 -2
- data/lib/foodcritic/api.rb +41 -23
- data/lib/foodcritic/command_line.rb +1 -21
- data/lib/foodcritic/domain.rb +0 -6
- data/lib/foodcritic/dsl.rb +1 -5
- data/lib/foodcritic/linter.rb +45 -30
- data/lib/foodcritic/rules.rb +43 -15
- data/lib/foodcritic/version.rb +1 -1
- metadata +16 -50
- data/lib/foodcritic/repl.rb +0 -28
data/lib/foodcritic.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'pathname'
|
2
2
|
require 'gherkin'
|
3
3
|
require 'treetop'
|
4
|
-
require 'pry'
|
5
4
|
require 'rak'
|
6
5
|
require 'ripper'
|
7
6
|
require 'yajl'
|
@@ -15,7 +14,6 @@ require_relative 'foodcritic/notifications'
|
|
15
14
|
require_relative 'foodcritic/ast'
|
16
15
|
require_relative 'foodcritic/xml'
|
17
16
|
require_relative 'foodcritic/api'
|
18
|
-
require_relative 'foodcritic/repl'
|
19
17
|
require_relative 'foodcritic/dsl'
|
20
18
|
require_relative 'foodcritic/linter'
|
21
19
|
require_relative 'foodcritic/output'
|
data/lib/foodcritic/api.rb
CHANGED
@@ -34,16 +34,18 @@ module FoodCritic
|
|
34
34
|
# Does the specified recipe check for Chef Solo?
|
35
35
|
def checks_for_chef_solo?(ast)
|
36
36
|
raise_unless_xpath!(ast)
|
37
|
-
|
38
37
|
# TODO: This expression is too loose, but also will fail to match other
|
39
38
|
# types of conditionals.
|
40
|
-
! ast.xpath(%q{//*[self::if or self::unless]/*[self::aref or
|
39
|
+
(! ast.xpath(%q{//*[self::if or self::unless]/*[self::aref or
|
40
|
+
child::aref or self::call]
|
41
41
|
[count(descendant::const[@value = 'Chef' or @value = 'Config']) = 2
|
42
42
|
and
|
43
43
|
( count(descendant::ident[@value='solo']) > 0
|
44
44
|
or count(descendant::tstring_content[@value='solo']) > 0
|
45
45
|
)
|
46
|
-
]}).empty?
|
46
|
+
]}).empty?) ||
|
47
|
+
ast.xpath('//if_mod[return][aref/descendant::ident/@value="solo"]/aref/
|
48
|
+
const_path_ref/descendant::const').map{|c|c['value']} == %w{Chef Config}
|
47
49
|
end
|
48
50
|
|
49
51
|
# Is the [chef-solo-search library](https://github.com/edelight/chef-solo-search)
|
@@ -105,12 +107,7 @@ module FoodCritic
|
|
105
107
|
# %w{foo bar baz}.each do |cbk|
|
106
108
|
# depends cbk
|
107
109
|
# end
|
108
|
-
|
109
|
-
descendant::var_ref/ident})
|
110
|
-
unless var_ref.empty?
|
111
|
-
deps += ast.xpath(%Q{//block_var/params/ident#{var_ref.first['value']}/
|
112
|
-
ancestor::method_add_block/call/descendant::tstring_content})
|
113
|
-
end
|
110
|
+
deps = deps.to_a + word_list_values(ast, "//command[ident/@value='depends']")
|
114
111
|
deps.map{|dep| dep['value']}
|
115
112
|
end
|
116
113
|
|
@@ -191,16 +188,6 @@ module FoodCritic
|
|
191
188
|
:line => pos['line'].to_i, :column => pos['column'].to_i}
|
192
189
|
end
|
193
190
|
|
194
|
-
# Does the provided string look like an Operating System command? This is a
|
195
|
-
# rough heuristic to be taken with a pinch of salt.
|
196
|
-
def os_command?(str)
|
197
|
-
str.start_with?('grep ', 'net ', 'which ') or # common commands
|
198
|
-
str.include?('|') or # a pipe, could be alternation
|
199
|
-
str.include?('/') or # file path delimiter
|
200
|
-
str.match(/^[\w]+$/) or # command name only
|
201
|
-
str.match(/ --?[a-z]/i) # command-line flag
|
202
|
-
end
|
203
|
-
|
204
191
|
# Read the AST for the given Ruby source file
|
205
192
|
def read_ast(file)
|
206
193
|
source = if file.to_s.end_with? '.erb'
|
@@ -298,6 +285,20 @@ module FoodCritic
|
|
298
285
|
templates}
|
299
286
|
end
|
300
287
|
|
288
|
+
# Platforms declared as supported in cookbook metadata
|
289
|
+
def supported_platforms(ast)
|
290
|
+
platforms = ast.xpath('//command[ident/@value="supports"]/
|
291
|
+
descendant::*[self::string_literal or self::symbol_literal][position() = 1]
|
292
|
+
[self::symbol_literal or count(descendant::string_add) = 1]/
|
293
|
+
descendant::*[self::tstring_content | self::ident]')
|
294
|
+
platforms = platforms.to_a + word_list_values(ast, "//command[ident/@value='supports']")
|
295
|
+
platforms.map do |platform|
|
296
|
+
versions = platform.xpath('ancestor::args_add[position() > 1]/
|
297
|
+
string_literal/descendant::tstring_content/@value').map{|v| v.to_s}
|
298
|
+
{:platform => platform['value'], :versions => versions}
|
299
|
+
end.sort{|a,b| a[:platform] <=> b[:platform]}
|
300
|
+
end
|
301
|
+
|
301
302
|
# Template filename
|
302
303
|
def template_file(resource)
|
303
304
|
if resource['source']
|
@@ -441,13 +442,20 @@ module FoodCritic
|
|
441
442
|
standard_attribute_access(ast, options.merge(:type => type))
|
442
443
|
end.inject(:+)
|
443
444
|
else
|
444
|
-
type = options[:type] == :string
|
445
|
-
|
446
|
-
|
445
|
+
type = if options[:type] == :string
|
446
|
+
'tstring_content'
|
447
|
+
else
|
448
|
+
'*[self::symbol or self::dyna_symbol]'
|
449
|
+
end
|
450
|
+
expr = '//*[self::aref_field or self::aref][count(method_add_arg) = 0]'
|
451
|
+
expr += '[count(is_att_type(descendant::var_ref/ident/@value)) =
|
452
|
+
count(descendant::var_ref/ident/@value)]'
|
447
453
|
expr += '[is_att_type(descendant::ident'
|
448
454
|
expr += '[not(ancestor::aref/call)]' if options[:ignore_calls]
|
449
455
|
expr += "/@value)]/descendant::#{type}"
|
450
|
-
|
456
|
+
if options[:type] == :string
|
457
|
+
expr += '[count(ancestor::dyna_symbol) = 0]'
|
458
|
+
end
|
451
459
|
ast.xpath(expr, AttFilter.new).sort
|
452
460
|
end
|
453
461
|
end
|
@@ -463,6 +471,16 @@ module FoodCritic
|
|
463
471
|
end.sort
|
464
472
|
end
|
465
473
|
|
474
|
+
def word_list_values(ast, xpath)
|
475
|
+
var_ref = ast.xpath("#{xpath}/descendant::var_ref/ident")
|
476
|
+
if var_ref.empty?
|
477
|
+
[]
|
478
|
+
else
|
479
|
+
ast.xpath(%Q{descendant::block_var/params/ident#{var_ref.first['value']}/
|
480
|
+
ancestor::method_add_block/call/descendant::tstring_content})
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
466
484
|
end
|
467
485
|
|
468
486
|
end
|
@@ -12,16 +12,12 @@ module FoodCritic
|
|
12
12
|
@options = {:fail_tags => [], :tags => [], :include_rules => []}
|
13
13
|
@parser = OptionParser.new do |opts|
|
14
14
|
opts.banner = 'foodcritic [cookbook_paths]'
|
15
|
-
opts.on("-r", "--[no-]repl",
|
16
|
-
"Drop into a REPL for interactive rule editing.") do |r|
|
17
|
-
options[:repl] = r
|
18
|
-
end
|
19
15
|
opts.on("-t", "--tags TAGS",
|
20
16
|
"Only check against rules with the specified tags.") do |t|
|
21
17
|
options[:tags] << t
|
22
18
|
end
|
23
19
|
opts.on("-f", "--epic-fail TAGS",
|
24
|
-
"Fail the build if any of the specified tags are matched.") do |t|
|
20
|
+
"Fail the build if any of the specified tags are matched ('any' -> fail on any match).") do |t|
|
25
21
|
options[:fail_tags] << t
|
26
22
|
end
|
27
23
|
opts.on("-c", "--chef-version VERSION",
|
@@ -86,14 +82,6 @@ module FoodCritic
|
|
86
82
|
"foodcritic #{FoodCritic::VERSION}"
|
87
83
|
end
|
88
84
|
|
89
|
-
# If the cookbook path provided is valid
|
90
|
-
#
|
91
|
-
# @deprecated Multiple cookbook paths may be provided.
|
92
|
-
# @return [Boolean] True if the path exists.
|
93
|
-
def valid_path?
|
94
|
-
@args.length == 1 and File.exists?(@args[0])
|
95
|
-
end
|
96
|
-
|
97
85
|
# If the cookbook paths provided are valid
|
98
86
|
#
|
99
87
|
# @return [Boolean] True if the paths exist.
|
@@ -101,14 +89,6 @@ module FoodCritic
|
|
101
89
|
@args.any? && @args.all? {|path| File.exists?(path) }
|
102
90
|
end
|
103
91
|
|
104
|
-
# The cookbook path.
|
105
|
-
#
|
106
|
-
# @deprecated Multiple cookbook paths may be provided.
|
107
|
-
# @return [String] Path to the cookbook(s) being checked.
|
108
|
-
def cookbook_path
|
109
|
-
@args[0]
|
110
|
-
end
|
111
|
-
|
112
92
|
# The cookbook paths
|
113
93
|
#
|
114
94
|
# @return [Array<String>] Path(s) to the cookbook(s) being checked.
|
data/lib/foodcritic/domain.rb
CHANGED
@@ -25,12 +25,6 @@ module FoodCritic
|
|
25
25
|
@is_failed = is_failed
|
26
26
|
end
|
27
27
|
|
28
|
-
# Provided for backwards compatibility. Deprecated and will be removed in a
|
29
|
-
# later version.
|
30
|
-
def cookbook_path
|
31
|
-
@cookbook_paths.first
|
32
|
-
end
|
33
|
-
|
34
28
|
# If this review has failed or not.
|
35
29
|
def failed?
|
36
30
|
@is_failed
|
data/lib/foodcritic/dsl.rb
CHANGED
@@ -65,17 +65,13 @@ module FoodCritic
|
|
65
65
|
rule_block :template
|
66
66
|
|
67
67
|
# Load the ruleset(s).
|
68
|
-
def self.load(paths
|
68
|
+
def self.load(paths)
|
69
69
|
dsl = RuleDsl.new
|
70
70
|
paths.map do |path|
|
71
71
|
File.directory?(path) ? Dir["#{path}/**/*.rb"].sort : path
|
72
72
|
end.flatten.each do |path|
|
73
73
|
dsl.instance_eval(File.read(path), path)
|
74
74
|
end
|
75
|
-
|
76
|
-
# Drop into the REPL for exploratory rule development.
|
77
|
-
dsl.instance_eval { binding.pry } if with_repl
|
78
|
-
|
79
75
|
dsl.rules
|
80
76
|
end
|
81
77
|
|
data/lib/foodcritic/linter.rb
CHANGED
@@ -9,7 +9,6 @@ module FoodCritic
|
|
9
9
|
class Linter
|
10
10
|
|
11
11
|
include FoodCritic::Api
|
12
|
-
include FoodCritic::REPL
|
13
12
|
|
14
13
|
# The default version that will be used to determine relevant rules. This
|
15
14
|
# can be over-ridden at the command line with the `--chef-version` option.
|
@@ -47,57 +46,73 @@ module FoodCritic
|
|
47
46
|
|
48
47
|
cookbook_paths = sanity_check_cookbook_paths(cookbook_paths)
|
49
48
|
options = setup_defaults(options)
|
49
|
+
@options = options
|
50
50
|
|
51
|
-
|
52
|
-
with_repl(cookbook_paths, options) do
|
53
|
-
warnings = []; last_dir = nil; matched_rule_tags = Set.new
|
51
|
+
warnings = []; last_dir = nil; matched_rule_tags = Set.new
|
54
52
|
|
55
|
-
|
53
|
+
load_rules
|
56
54
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
55
|
+
# Loop through each file to be processed and apply the rules
|
56
|
+
files_to_process(cookbook_paths, options[:exclude_paths]).each do |file|
|
57
|
+
ast = read_ast(file)
|
58
|
+
active_rules(options).each do |rule|
|
59
|
+
rule_matches = matches(rule.recipe, ast, file)
|
62
60
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
if dsl_method_for_file(file)
|
62
|
+
rule_matches += matches(rule.send(dsl_method_for_file(file)),
|
63
|
+
ast, file)
|
64
|
+
end
|
67
65
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
end
|
72
|
-
rule_matches += matches(rule.cookbook, cookbook_dir(file))
|
66
|
+
per_cookbook_rules(last_dir, file) do
|
67
|
+
if File.basename(file) == 'metadata.rb'
|
68
|
+
rule_matches += matches(rule.metadata, ast, file)
|
73
69
|
end
|
70
|
+
rule_matches += matches(rule.cookbook, cookbook_dir(file))
|
71
|
+
end
|
74
72
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
73
|
+
rule_matches = remove_ignored(rule_matches, rule, file)
|
74
|
+
|
75
|
+
# Convert the matches into warnings
|
76
|
+
rule_matches.each do |match|
|
77
|
+
warnings << Warning.new(rule, {:filename => file}.merge(match))
|
78
|
+
matched_rule_tags << rule.tags
|
80
79
|
end
|
81
|
-
last_dir = cookbook_dir(file)
|
82
80
|
end
|
83
|
-
|
84
|
-
Review.new(cookbook_paths, warnings,
|
85
|
-
should_fail_build?(options[:fail_tags], matched_rule_tags))
|
81
|
+
last_dir = cookbook_dir(file)
|
86
82
|
end
|
83
|
+
|
84
|
+
Review.new(cookbook_paths, warnings,
|
85
|
+
should_fail_build?(options[:fail_tags], matched_rule_tags))
|
87
86
|
end
|
88
87
|
|
89
88
|
# Load the rules from the (fairly unnecessary) DSL.
|
90
89
|
def load_rules
|
91
|
-
load_rules!(@
|
90
|
+
load_rules!(@options) unless defined? @rules
|
92
91
|
end
|
93
92
|
|
94
93
|
def load_rules!(options)
|
95
94
|
@rules = RuleDsl.load([File.join(File.dirname(__FILE__), 'rules.rb')] +
|
96
|
-
options[:include_rules]
|
95
|
+
options[:include_rules])
|
97
96
|
end
|
98
97
|
|
99
98
|
private
|
100
99
|
|
100
|
+
def remove_ignored(matches, rule, file)
|
101
|
+
matches.reject do |m|
|
102
|
+
(line = m[:line]) && File.exist?(file) &&
|
103
|
+
ignore_line_match?(File.readlines(file)[line-1], rule)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def ignore_line_match?(line, rule)
|
108
|
+
ignores = line.to_s[/\s+#\s*(.*)/, 1]
|
109
|
+
if ignores and ignores.include?('~')
|
110
|
+
! matching_tags?(ignores.split(/[ ,]/), rule.tags)
|
111
|
+
else
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
101
116
|
# Some rules are version specific.
|
102
117
|
def applies_to_version?(rule, version)
|
103
118
|
return true unless version
|
data/lib/foodcritic/rules.rb
CHANGED
@@ -207,7 +207,8 @@ rule "FC017", "LWRP does not notify when updated" do
|
|
207
207
|
version >= gem_version("0.7.12")
|
208
208
|
end
|
209
209
|
provider do |ast, filename|
|
210
|
-
if ast.xpath(%q{
|
210
|
+
if ast.xpath(%q{//*[self::call or self::command_call]/
|
211
|
+
*[self::vcall or self::var_ref/ident/
|
211
212
|
@value='new_resource']/../
|
212
213
|
ident[@value='updated_by_last_action']}).empty?
|
213
214
|
[file_match(filename)]
|
@@ -229,23 +230,26 @@ end
|
|
229
230
|
rule "FC019", "Access node attributes in a consistent manner" do
|
230
231
|
tags %w{style attributes}
|
231
232
|
cookbook do |cookbook_dir|
|
232
|
-
asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].
|
233
|
+
asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].reject do |file|
|
234
|
+
relative_path = Pathname.new(file).relative_path_from(Pathname.new(cookbook_dir))
|
235
|
+
relative_path.to_s.split(File::SEPARATOR).include?('spec')
|
236
|
+
end.map do |file|
|
233
237
|
{:path => file, :ast => read_ast(file)}
|
234
238
|
end
|
235
239
|
types = [:string, :symbol, :vivified].map do |type|
|
236
240
|
{:access_type => type, :count => files.map do |file|
|
237
241
|
attribute_access(file[:ast], :type => type, :ignore_calls => true,
|
238
242
|
:cookbook_dir => cookbook_dir).tap do |ast|
|
239
|
-
|
240
|
-
asts[type]
|
243
|
+
unless ast.empty?
|
244
|
+
(asts[type] ||= []) << {:ast => ast, :path => file[:path]}
|
241
245
|
end
|
242
246
|
end.size
|
243
247
|
end.inject(:+)}
|
244
248
|
end.reject{|type| type[:count] == 0}
|
245
249
|
if asts.size > 1
|
246
250
|
least_used = asts[types.min{|a,b| a[:count] <=> b[:count]}[:access_type]]
|
247
|
-
least_used
|
248
|
-
match(ast).merge(:filename =>
|
251
|
+
least_used.map do |file|
|
252
|
+
file[:ast].map{|ast| match(ast).merge(:filename => file[:path])}.flatten
|
249
253
|
end
|
250
254
|
end
|
251
255
|
end
|
@@ -307,17 +311,26 @@ end
|
|
307
311
|
rule "FC024", "Consider adding platform equivalents" do
|
308
312
|
tags %w{portability}
|
309
313
|
RHEL = %w{amazon centos redhat scientific}
|
310
|
-
recipe do |ast|
|
314
|
+
recipe do |ast, filename|
|
315
|
+
next if Pathname.new(filename).basename.to_s == 'metadata.rb'
|
316
|
+
metadata_path = Pathname.new(
|
317
|
+
File.join(File.dirname(filename), '..', 'metadata.rb')).cleanpath
|
318
|
+
md_platforms = if File.exists?(metadata_path)
|
319
|
+
supported_platforms(read_ast(metadata_path)).map{|p| p[:platform]}
|
320
|
+
else
|
321
|
+
[]
|
322
|
+
end
|
323
|
+
md_platforms = RHEL if md_platforms.empty?
|
324
|
+
|
311
325
|
['//method_add_arg[fcall/ident/@value="platform?"]/arg_paren/args_add_block',
|
312
326
|
"//when"].map do |expr|
|
313
327
|
ast.xpath(expr).map do |whn|
|
314
328
|
platforms = whn.xpath("args_add/descendant::tstring_content").map do |p|
|
315
329
|
p['value']
|
316
|
-
end
|
317
|
-
unless platforms.size == 1 || (
|
318
|
-
unless (
|
319
|
-
|
320
|
-
end
|
330
|
+
end.sort
|
331
|
+
unless platforms.size == 1 || (md_platforms & platforms).empty?
|
332
|
+
whn unless (platforms & RHEL).empty? ||
|
333
|
+
((md_platforms & RHEL) - (platforms & RHEL)).empty?
|
321
334
|
end
|
322
335
|
end.compact
|
323
336
|
end.flatten
|
@@ -476,7 +489,7 @@ rule "FC037", "Invalid notification action" do
|
|
476
489
|
when :notifies then n[:resource_type]
|
477
490
|
when :subscribes then resource_type(resource).to_sym
|
478
491
|
end
|
479
|
-
! resource_action?(type, n[:action])
|
492
|
+
n[:action].size > 0 and ! resource_action?(type, n[:action])
|
480
493
|
end
|
481
494
|
end
|
482
495
|
end
|
@@ -522,9 +535,14 @@ end
|
|
522
535
|
rule "FC040", "Execute resource used to run git commands" do
|
523
536
|
tags %w{style recipe etsy}
|
524
537
|
recipe do |ast|
|
538
|
+
possible_git_commands = %w{ clone fetch pull checkout reset }
|
525
539
|
find_resources(ast, :type => 'execute').select do |cmd|
|
526
540
|
cmd_str = (resource_attribute(cmd, 'command') || resource_name(cmd)).to_s
|
527
|
-
|
541
|
+
|
542
|
+
git_cmd = cmd_str.match(/git ([a-z]+)/)
|
543
|
+
break false if git_cmd.nil?
|
544
|
+
|
545
|
+
!git_cmd.captures.nil? && possible_git_commands.include?(git_cmd.captures[0])
|
528
546
|
end
|
529
547
|
end
|
530
548
|
end
|
@@ -548,6 +566,7 @@ end
|
|
548
566
|
|
549
567
|
rule "FC043", "Prefer new notification syntax" do
|
550
568
|
tags %w{style notifications deprecated}
|
569
|
+
applies_to {|version| version >= gem_version("0.9.10")}
|
551
570
|
recipe do |ast|
|
552
571
|
find_resources(ast).select do |resource|
|
553
572
|
notifications(resource).any?{|notify| notify[:style] == :old}
|
@@ -561,7 +580,10 @@ rule "FC044", "Avoid bare attribute keys" do
|
|
561
580
|
declared = ast.xpath('//descendant::var_field/ident/@value').map{|v| v.to_s}
|
562
581
|
ast.xpath('//assign/*[self::vcall or self::var_ref]
|
563
582
|
[count(child::kw) = 0]/ident').select do |v|
|
564
|
-
(v['value'] != 'secure_password') &&
|
583
|
+
(v['value'] != 'secure_password') &&
|
584
|
+
! declared.include?(v['value']) &&
|
585
|
+
! v.xpath("ancestor::*[self::brace_block or self::do_block]/block_var/
|
586
|
+
descendant::ident/@value='#{v['value']}'")
|
565
587
|
end
|
566
588
|
end
|
567
589
|
end
|
@@ -579,3 +601,9 @@ rule "FC045", "Consider setting cookbook name in metadata" do
|
|
579
601
|
end
|
580
602
|
end
|
581
603
|
end
|
604
|
+
|
605
|
+
rule "FC046", "Attribute assignment uses assign unless nil" do
|
606
|
+
attributes do |ast|
|
607
|
+
attribute_access(ast).map{|a| a.xpath('ancestor::opassign/op[@value="||="]')}
|
608
|
+
end
|
609
|
+
end
|
data/lib/foodcritic/version.rb
CHANGED