foodcritic 0.1.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 +8 -0
- data/lib/foodcritic.rb +5 -0
- data/lib/foodcritic/domain.rb +52 -0
- data/lib/foodcritic/dsl.rb +44 -0
- data/lib/foodcritic/helpers.rb +106 -0
- data/lib/foodcritic/linter.rb +47 -0
- data/lib/foodcritic/rules.rb +82 -0
- data/lib/foodcritic/version.rb +3 -0
- metadata +58 -0
data/bin/foodcritic
ADDED
data/lib/foodcritic.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module FoodCritic
|
|
2
|
+
|
|
3
|
+
# A warning of a possible issue
|
|
4
|
+
class Warning
|
|
5
|
+
attr_reader :rule, :match
|
|
6
|
+
|
|
7
|
+
# Create a new warning
|
|
8
|
+
#
|
|
9
|
+
# @param [FoodCritic::Rule] rule The rule which raised this warning
|
|
10
|
+
# @param [Hash] match The match data
|
|
11
|
+
# @option match [String] :filename The filename the warning was raised against
|
|
12
|
+
# @option match [Integer] :line The identified line
|
|
13
|
+
# @option match [Integer] :column The identified column
|
|
14
|
+
def initialize(rule, match={})
|
|
15
|
+
@rule, @match = rule, match
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The collected warnings (if any) raised against a cookbook tree.
|
|
20
|
+
class Review
|
|
21
|
+
|
|
22
|
+
attr_reader :warnings
|
|
23
|
+
|
|
24
|
+
# Create a new review
|
|
25
|
+
#
|
|
26
|
+
# @param [Array] warnings The warnings raised in this review
|
|
27
|
+
def initialize(warnings)
|
|
28
|
+
@warnings = warnings
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns a string representation of this review.
|
|
32
|
+
#
|
|
33
|
+
# @return [String] Review as a string, this representation is liable to change.
|
|
34
|
+
def to_s
|
|
35
|
+
@warnings.map { |w| "#{w.rule.code}: #{w.rule.name}: #{w.match[:filename]}:#{w.match[:line]}" }.sort.uniq.join("\n")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A rule to be matched against.
|
|
40
|
+
class Rule
|
|
41
|
+
attr_accessor :code, :name, :description, :recipe
|
|
42
|
+
|
|
43
|
+
# Create a new rule
|
|
44
|
+
#
|
|
45
|
+
# @param [String] code The short unique identifier for this rule, e.g. 'FC001'
|
|
46
|
+
# @param [String] name The short descriptive name of this rule presented to the end user.
|
|
47
|
+
def initialize(code, name)
|
|
48
|
+
@code, @name = code, name
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module FoodCritic
|
|
2
|
+
|
|
3
|
+
# The DSL methods exposed for defining rules.
|
|
4
|
+
class RuleDsl
|
|
5
|
+
attr_reader :rules
|
|
6
|
+
include Helpers
|
|
7
|
+
|
|
8
|
+
# Define a new rule
|
|
9
|
+
#
|
|
10
|
+
# @param [String] code The short unique identifier for this rule, e.g. 'FC001'
|
|
11
|
+
# @param [String] name The short descriptive name of this rule presented to the end user.
|
|
12
|
+
# @param [Block] block The rule definition
|
|
13
|
+
def rule(code, name, &block)
|
|
14
|
+
@rules = [] if @rules.nil?
|
|
15
|
+
@rules << Rule.new(code, name)
|
|
16
|
+
yield self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Set the rule description
|
|
20
|
+
#
|
|
21
|
+
# @param [String] description Set the rule description.
|
|
22
|
+
def description(description)
|
|
23
|
+
rules.last.description = description
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Define a matcher that will be passed the AST with this method.
|
|
27
|
+
#
|
|
28
|
+
# @param [block] block Your implemented matcher that returns a match Hash.
|
|
29
|
+
def recipe(&block)
|
|
30
|
+
rules.last.recipe = block
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Load the ruleset
|
|
34
|
+
#
|
|
35
|
+
# @param [String] filename The path to the ruleset to load
|
|
36
|
+
# @return [Array] The loaded rules, ready to be matched against provided cookbooks.
|
|
37
|
+
def self.load(filename)
|
|
38
|
+
dsl = RuleDsl.new
|
|
39
|
+
dsl.instance_eval(File.read(filename), filename)
|
|
40
|
+
dsl.rules
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module FoodCritic
|
|
2
|
+
|
|
3
|
+
# Helper methods that form part of the Rules DSL.
|
|
4
|
+
module Helpers
|
|
5
|
+
|
|
6
|
+
# Given an AST type and parsed tree, return the matching subset.
|
|
7
|
+
#
|
|
8
|
+
# @param [Symbol] type The type of AST node to look for
|
|
9
|
+
# @param [Array] node The parsed AST (or part there-of)
|
|
10
|
+
# @return [Array] Matching nodes
|
|
11
|
+
def ast(type, node)
|
|
12
|
+
result = []
|
|
13
|
+
result = [node] if node.first == type
|
|
14
|
+
node.each { |n| result += ast(type, n) if n.respond_to?(:each) }
|
|
15
|
+
result
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Does the specified recipe check for Chef Solo?
|
|
19
|
+
#
|
|
20
|
+
# @param [Array] ast The AST of the cookbook recipe to check.
|
|
21
|
+
# @return [Boolean] True if there is a test for Chef::Config[:solo] in the recipe
|
|
22
|
+
def checks_for_chef_solo?(ast)
|
|
23
|
+
arefs = ast(:aref, ast)
|
|
24
|
+
arefs.any? do |aref|
|
|
25
|
+
ast(:@const, aref).map { |const| const[1] } == ['Chef', 'Config'] and
|
|
26
|
+
ast(:@ident, ast(:symbol, aref)).map { |sym| sym.drop(1).first }.include? 'solo'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Find Chef resources of the specified type.
|
|
31
|
+
# TODO: Include blockless resources
|
|
32
|
+
#
|
|
33
|
+
# @param [Array] ast The AST of the cookbook recipe to check
|
|
34
|
+
# @param [String] type The type of resource to look for (or nil for all resources)
|
|
35
|
+
def find_resources(ast, type = nil)
|
|
36
|
+
ast(:method_add_block, ast).find_all do |resource|
|
|
37
|
+
resource[1][0] == :command and resource[1][1][0] == :@ident and (type.nil? || resource[1][1][1] == type)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Return the type, e.g. 'package' for a given resource
|
|
42
|
+
#
|
|
43
|
+
# @param [Array] resource The resource AST
|
|
44
|
+
# @return [String] The type of resource
|
|
45
|
+
def resource_type(resource)
|
|
46
|
+
resource[1][1][1]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Retrieve the name attribute associated with the specified resource.
|
|
50
|
+
#
|
|
51
|
+
# @param [Array] resource The resource AST to lookup the name attribute under
|
|
52
|
+
def resource_name(resource)
|
|
53
|
+
ast(:@tstring_content, resource[1]).flatten[1]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Retrieve a single-valued attribute from the specified resource.
|
|
57
|
+
#
|
|
58
|
+
# @param [String] name The attribute name
|
|
59
|
+
# @param [Array] resource The resource AST to lookup the attribute under
|
|
60
|
+
# @return [String] The attribute value for the specified attribute
|
|
61
|
+
def resource_attribute(name, resource)
|
|
62
|
+
cmd = ast(:command, ast(:do_block, resource))
|
|
63
|
+
atts = cmd.find_all { |att| ast(:@ident, att).flatten.drop(1).first == name }
|
|
64
|
+
value = ast(:@tstring_content, atts).flatten.drop(1)
|
|
65
|
+
unless value.empty?
|
|
66
|
+
return value.first
|
|
67
|
+
end
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Retrieve all attributes from the specified resource.
|
|
72
|
+
#
|
|
73
|
+
# @param [Array] resource The resource AST
|
|
74
|
+
# @return [Hash] The resource attributes
|
|
75
|
+
def resource_attributes(resource)
|
|
76
|
+
atts = {:name => resource_name(resource)}
|
|
77
|
+
ast(:command, ast(:do_block, resource)).find_all{|cmd| cmd.first == :command}.each do |cmd|
|
|
78
|
+
atts[cmd[1][1]] = ast(:@tstring_content, cmd[2]).flatten[1] || ast(:@ident, cmd[2]).flatten[1]
|
|
79
|
+
end
|
|
80
|
+
atts
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Retrieve all resources of a given type
|
|
84
|
+
#
|
|
85
|
+
# @param [Array] ast The recipe AST
|
|
86
|
+
# @return [Array] The matching resources
|
|
87
|
+
def resources_by_type(ast)
|
|
88
|
+
result = Hash.new{|hash, key| hash[key] = Array.new}
|
|
89
|
+
find_resources(ast).each{|resource| result[resource_type(resource)] << resource}
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Retrieve the attributes as a hash for all resources of a given type.
|
|
94
|
+
#
|
|
95
|
+
# @param [Array] ast The recipe AST
|
|
96
|
+
# @return [Hash] An array of resource attributes keyed by type.
|
|
97
|
+
def resource_attributes_by_type(ast)
|
|
98
|
+
result = {}
|
|
99
|
+
resources_by_type(ast).each do |type,resources|
|
|
100
|
+
result[type] = resources.map{|resource| resource_attributes(resource)}
|
|
101
|
+
end
|
|
102
|
+
result
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'ripper'
|
|
2
|
+
|
|
3
|
+
module FoodCritic
|
|
4
|
+
|
|
5
|
+
# The main entry point for linting your Chef cookbooks.
|
|
6
|
+
class Linter
|
|
7
|
+
|
|
8
|
+
# Create a new Linter, loading any defined rules.
|
|
9
|
+
def initialize
|
|
10
|
+
load_rules
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Review the cookbooks at the provided path, identifying potential improvements.
|
|
14
|
+
#
|
|
15
|
+
# @param [String] cookbook_path The file path to an individual cookbook directory
|
|
16
|
+
# @return [FoodCritic::Review] A review of your cookbooks, with any warnings issued.
|
|
17
|
+
def check(cookbook_path)
|
|
18
|
+
warnings = []
|
|
19
|
+
files_to_process(cookbook_path).each do |file|
|
|
20
|
+
ast = Ripper::SexpBuilder.new(IO.read(file)).parse
|
|
21
|
+
@rules.each do |rule|
|
|
22
|
+
rule.recipe.yield(ast).each do |match|
|
|
23
|
+
warnings << Warning.new(rule, match.merge({:filename => file}))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
Review.new(warnings)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Load the rules from the (fairly unnecessary) DSL.
|
|
33
|
+
def load_rules
|
|
34
|
+
@rules = RuleDsl.load(File.join(File.dirname(__FILE__), 'rules.rb'))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Return the files within a cookbook tree that we are interested in trying to match rules against.
|
|
38
|
+
#
|
|
39
|
+
# @param [String] dir The cookbook directory
|
|
40
|
+
# @return [Array] The files underneath the provided directory to be processed.
|
|
41
|
+
def files_to_process(dir)
|
|
42
|
+
return [dir] unless File.directory? dir
|
|
43
|
+
Dir.glob(File.join(dir, '{attributes,recipes}/*.rb')) + Dir.glob(File.join(dir, '*/{attributes,recipes}/*.rb'))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
rule "FC001", "Use symbols in preference to strings to access node attributes" do
|
|
2
|
+
description "When accessing node attributes you should use a symbol for a key rather than a string literal."
|
|
3
|
+
recipe do |ast|
|
|
4
|
+
matches = []
|
|
5
|
+
attribute_refs = %w{node default override set normal}
|
|
6
|
+
aref_fields = self.ast(:aref, ast) + self.ast(:aref_field, ast)
|
|
7
|
+
aref_fields.each do |field|
|
|
8
|
+
is_node_aref = attribute_refs.include? self.ast(:@ident, field).flatten.drop(1).first
|
|
9
|
+
if is_node_aref
|
|
10
|
+
literal_strings = self.ast(:@tstring_content, field)
|
|
11
|
+
literal_strings.each do |str|
|
|
12
|
+
matches << {:matched => str[1], :line => str[2].first, :column => str[2].last}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
matches
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
rule "FC002", "Avoid string interpolation where not required" do
|
|
21
|
+
description "When setting a resource value avoid string interpolation where not required."
|
|
22
|
+
recipe do |ast|
|
|
23
|
+
matches = []
|
|
24
|
+
self.ast(:string_literal, ast).each do |literal|
|
|
25
|
+
embed_expr = self.ast(:string_embexpr, literal)
|
|
26
|
+
if embed_expr.size == 1
|
|
27
|
+
literal[1].reject! { |expr| expr == embed_expr.first }
|
|
28
|
+
if self.ast(:@tstring_content, literal).empty?
|
|
29
|
+
self.ast(:@ident, embed_expr).map { |ident| ident.flatten.drop(1) }.each do |ident|
|
|
30
|
+
matches << {:matched => ident[0], :line => ident[1], :column => ident[2]}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
matches
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
rule "FC003", "Check whether you are running with chef server before using server-specific features" do
|
|
40
|
+
description "Ideally your cookbooks should be usable without requiring chef server."
|
|
41
|
+
recipe do |ast|
|
|
42
|
+
matches = []
|
|
43
|
+
function_calls = self.ast(:@ident, self.ast(:fcall, ast)).map { |fcall| fcall.drop(1).flatten }
|
|
44
|
+
searches = function_calls.find_all { |fcall| fcall.first == 'search' }
|
|
45
|
+
unless searches.empty? || checks_for_chef_solo?(ast)
|
|
46
|
+
searches.each { |s| matches << {:matched => s[0], :line => s[1], :column => s[2]} }
|
|
47
|
+
end
|
|
48
|
+
matches
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
rule "FC004", "Use a service resource to start and stop services" do
|
|
53
|
+
description "Avoid use of execute to control services - use the service resource instead."
|
|
54
|
+
recipe do |ast|
|
|
55
|
+
matches = []
|
|
56
|
+
find_resources(ast, 'execute').find_all do |cmd|
|
|
57
|
+
cmd_str = resource_attribute('command', cmd)
|
|
58
|
+
cmd_str = resource_name(cmd) if cmd_str.nil?
|
|
59
|
+
cmd_str.include?('/etc/init.d') || cmd_str.start_with?('service ') || cmd_str.start_with?('/sbin/service ')
|
|
60
|
+
end.each do |service_cmd|
|
|
61
|
+
exec = ast(:@ident, service_cmd).first.drop(1).flatten
|
|
62
|
+
matches << {:matched => exec[0], :line => exec[1], :column => exec[2]}
|
|
63
|
+
end
|
|
64
|
+
matches
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
rule "FC005", "Avoid repetition of resource declarations" do
|
|
69
|
+
description "Where you have a lot of resources that vary in only a single attribute wrap them in a loop for brevity."
|
|
70
|
+
recipe do |ast|
|
|
71
|
+
matches = []
|
|
72
|
+
# do all of the attributes for all resources of a given type match apart aside from one?
|
|
73
|
+
resource_attributes_by_type(ast).each do |type, resource_atts|
|
|
74
|
+
sorted_atts = resource_atts.map{|atts| atts.to_a.sort{|x,y| x.first.to_s <=> y.first.to_s }}
|
|
75
|
+
if sorted_atts.all?{|att| (att - sorted_atts.inject{|atts,a| atts & a}).length == 1}
|
|
76
|
+
first_resource = ast(:@ident, find_resources(ast, type).first).first[2]
|
|
77
|
+
matches << {:matched => type, :line => first_resource[0], :column => first_resource[1]}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
matches
|
|
81
|
+
end
|
|
82
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: foodcritic
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Andrew Crump
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2011-11-30 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: Lint tool for Opscode Chef cookbooks.
|
|
15
|
+
email:
|
|
16
|
+
executables:
|
|
17
|
+
- foodcritic
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- lib/foodcritic/domain.rb
|
|
22
|
+
- lib/foodcritic/dsl.rb
|
|
23
|
+
- lib/foodcritic/helpers.rb
|
|
24
|
+
- lib/foodcritic/linter.rb
|
|
25
|
+
- lib/foodcritic/rules.rb
|
|
26
|
+
- lib/foodcritic/version.rb
|
|
27
|
+
- lib/foodcritic.rb
|
|
28
|
+
- bin/foodcritic
|
|
29
|
+
homepage: http://acrmp.github.com/foodcritic
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
post_install_message:
|
|
33
|
+
rdoc_options: []
|
|
34
|
+
require_paths:
|
|
35
|
+
- lib
|
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
37
|
+
none: false
|
|
38
|
+
requirements:
|
|
39
|
+
- - ! '>='
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: 1.9.3
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
none: false
|
|
44
|
+
requirements:
|
|
45
|
+
- - ! '>='
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
segments:
|
|
49
|
+
- 0
|
|
50
|
+
hash: -3064369866529469522
|
|
51
|
+
requirements: []
|
|
52
|
+
rubyforge_project:
|
|
53
|
+
rubygems_version: 1.8.10
|
|
54
|
+
signing_key:
|
|
55
|
+
specification_version: 3
|
|
56
|
+
summary: foodcritic-0.1.0
|
|
57
|
+
test_files: []
|
|
58
|
+
has_rdoc:
|