Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial version.

  • Loading branch information...
commit 4257264aa0bf93e3d13851ac0343a6b25ca3d316 0 parents
@acrmp authored
6 .gitignore
@@ -0,0 +1,6 @@
+.idea
+.yardoc
+coverage
+doc
+pkg
+tmp
3  CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0 (27th November, 2011)
+
+Initial version.
17 Gemfile
@@ -0,0 +1,17 @@
+source "http://rubygems.org"
+
+gem 'foodcritic'
+
+group :test do
+ gem 'aruba', '~> 0.4.7'
+ gem 'cucumber', '~> 1.1.3'
+ gem 'simplecov', '~> 0.5.4'
+end
+
+group :development do
+ gem 'guard', '~> 0.8.8'
+ gem 'guard-cucumber', '~> 0.7.4'
+ gem "rake", '~> 0.9.2.2'
+ gem "rdiscount", '~> 1.6.8'
+ gem "yard", '~> 0.7.3'
+end
60 Gemfile.lock
@@ -0,0 +1,60 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ aruba (0.4.7)
+ childprocess (>= 0.2.2)
+ cucumber (>= 1.1.1)
+ ffi (= 1.0.9)
+ rspec (>= 2.7.0)
+ builder (3.0.0)
+ childprocess (0.2.2)
+ ffi (~> 1.0.6)
+ cucumber (1.1.3)
+ builder (>= 2.1.2)
+ diff-lcs (>= 1.1.2)
+ gherkin (~> 2.6.7)
+ json (>= 1.4.6)
+ term-ansicolor (>= 1.0.6)
+ diff-lcs (1.1.3)
+ ffi (1.0.9)
+ foodcritic (0.1.0)
+ gherkin (2.6.8)
+ json (>= 1.4.6)
+ guard (0.8.8)
+ thor (~> 0.14.6)
+ guard-cucumber (0.7.4)
+ cucumber (>= 0.10)
+ guard (>= 0.8.3)
+ json (1.6.1)
+ multi_json (1.0.3)
+ rake (0.9.2.2)
+ rdiscount (1.6.8)
+ rspec (2.7.0)
+ rspec-core (~> 2.7.0)
+ rspec-expectations (~> 2.7.0)
+ rspec-mocks (~> 2.7.0)
+ rspec-core (2.7.1)
+ rspec-expectations (2.7.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.7.0)
+ simplecov (0.5.4)
+ multi_json (~> 1.0.3)
+ simplecov-html (~> 0.5.3)
+ simplecov-html (0.5.3)
+ term-ansicolor (1.0.7)
+ thor (0.14.6)
+ yard (0.7.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ aruba (~> 0.4.7)
+ cucumber (~> 1.1.3)
+ foodcritic
+ guard (~> 0.8.8)
+ guard-cucumber (~> 0.7.4)
+ rake (~> 0.9.2.2)
+ rdiscount (~> 1.6.8)
+ simplecov (~> 0.5.4)
+ yard (~> 0.7.3)
21 LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (C) 2011 by Andrew Crump
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
18 README.md
@@ -0,0 +1,18 @@
+# Food Critic
+
+Food Critic is a lint tool for Chef cookbooks. It requires Ruby 1.9.3.
+
+# Building
+
+ $ bundle install
+ $ bundle exec rake
+
+# License
+MIT - see the accompanying [LICENSE](https://github.com/acrmp/foodcritic/blob/master/LICENSE) file for details.
+
+# Changelog
+To see what has changed in recent versions see the [CHANGELOG](https://github.com/acrmp/foodcritic/blob/master/CHANGELOG.md).
+Food Critic follows the [Rubygems RationalVersioningPolicy](http://docs.rubygems.org/read/chapter/7).
+
+# Contributing
+Additional rules and bugfixes are welcome! Please fork and submit a pull request on an individual branch per change.
15 Rakefile
@@ -0,0 +1,15 @@
+require 'rubygems'
+require 'bundler'
+require 'cucumber'
+require 'cucumber/rake/task'
+require 'yard'
+task :default => [:install, :features]
+
+Bundler.setup
+Bundler::GemHelper.install_tasks
+
+Cucumber::Rake::Task.new(:features) do |t|
+ t.cucumber_opts = "features --format pretty"
+end
+
+YARD::Rake::YardocTask.new
3  bin/foodcritic
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require 'foodcritic'
+puts FoodCritic::Linter.new.check(ARGV[0])
50 features/001_check_node_access.feature
@@ -0,0 +1,50 @@
+Feature: Check Node Access
+
+ In order to make my cookbooks more readable
+ As a developer
+ I want to identify if the cookbooks access node attributes with strings rather than symbols
+
+ Scenario: Cookbook recipe accesses attributes via strings
+ Given a cookbook with a single recipe that accesses node attributes via strings
+ When I check the cookbook
+ Then the node access warning 001 should be displayed
+
+ Scenario: Cookbook recipe accesses multiple attributes via strings
+ Given a cookbook with a single recipe that accesses multiple node attributes via strings
+ When I check the cookbook
+ Then the node access warning 001 should be displayed for each match
+
+ Scenario: Assignment of node attributes accessed via strings
+ Given a cookbook with a single recipe that assigns node attributes accessed via strings to a local variable
+ When I check the cookbook
+ Then the node access warning 001 should be displayed
+
+ Scenario: Cookbook recipe accesses nested attributes via strings
+ Given a cookbook with a single recipe that accesses nested node attributes via strings
+ When I check the cookbook
+ Then the node access warning 001 should be displayed twice for the same line
+
+ Scenario: Cookbook recipe accesses attributes via symbols
+ Given a cookbook with a single recipe that accesses node attributes via symbols
+ When I check the cookbook
+ Then the node access warning 001 should not be displayed
+
+ Scenario: Cookbook recipe sets default attributes via strings
+ Given a cookbook that declares default attributes via strings
+ When I check the cookbook
+ Then the node access warning 001 should be displayed against the attributes file
+
+ Scenario: Cookbook recipe overrides attributes via strings
+ Given a cookbook that declares override attributes via strings
+ When I check the cookbook
+ Then the node access warning 001 should be displayed against the attributes file
+
+ Scenario: Cookbook recipe sets attributes via strings
+ Given a cookbook that declares set attributes via strings
+ When I check the cookbook
+ Then the node access warning 001 should be displayed against the attributes file
+
+ Scenario: Cookbook recipe sets normal attributes via strings
+ Given a cookbook that declares normal attributes via strings
+ When I check the cookbook
+ Then the node access warning 001 should be displayed against the attributes file
30 features/002_check_string_interpolation.feature
@@ -0,0 +1,30 @@
+Feature: Check String Interpolation
+
+ In order to make my cookbooks more readable
+ As a developer
+ I want to identify if values are unnecessarily interpolated
+
+ Scenario: Resource name interpolated string (symbol)
+ Given a cookbook with a single recipe that creates a directory resource with an interpolated name
+ When I check the cookbook
+ Then the string interpolation warning 002 should be displayed
+
+ Scenario: Resource name interpolated string
+ Given a cookbook with a single recipe that creates a directory resource with an interpolated name from a string
+ When I check the cookbook
+ Then the string interpolation warning 002 should be displayed
+
+ Scenario: Resource name string literal
+ Given a cookbook with a single recipe that creates a directory resource with a string literal
+ When I check the cookbook
+ Then the string interpolation warning 002 should not be displayed
+
+ Scenario: Resource name compound expression
+ Given a cookbook with a single recipe that creates a directory resource with a compound expression
+ When I check the cookbook
+ Then the string interpolation warning 002 should not be displayed
+
+ Scenario: Resource name literal and expression
+ Given a cookbook with a single recipe that creates a directory resource with an interpolated literal and expression
+ When I check the cookbook
+ Then the string interpolation warning 002 should not be displayed
15 features/003_check_for_chef_server.feature
@@ -0,0 +1,15 @@
+Feature: Check for Chef Server
+
+ In order to ensure my cookbooks can be run with chef solo
+ As a developer
+ I want to identify if server-only features are used without checking to see if this is server
+
+ Scenario: Search without checking for server
+ Given a cookbook with a single recipe that searches without checking if this is server
+ When I check the cookbook
+ Then the check for server warning 003 should be displayed
+
+ Scenario: Search checking for server
+ Given a cookbook with a single recipe that searches but checks first to see if this is server
+ When I check the cookbook
+ Then the check for server warning 003 should not be displayed given we have checked
30 features/004_check_service_resource_used.feature
@@ -0,0 +1,30 @@
+Feature: Check for service commands within execute resources
+
+ In order to control services in an idiomatic way
+ As a developer
+ I want to identify if service commands are called by execute resources rather than using the service resource
+
+ Scenario: Execute resource starting a service via init.d
+ Given a cookbook recipe that uses execute to start a service via init.d
+ When I check the cookbook
+ Then the service resource warning 004 should be displayed
+
+ Scenario: Execute resource starting a service via service command
+ Given a cookbook recipe that uses execute to start a service via the service command
+ When I check the cookbook
+ Then the service resource warning 004 should be displayed
+
+ Scenario: Execute resource starting a service via the full path to the service command
+ Given a cookbook recipe that uses execute to start a service via full path to the service command
+ When I check the cookbook
+ Then the service resource warning 004 should be displayed
+
+ Scenario: Execute resource starting a service via init.d (multiple commands)
+ Given a cookbook recipe that uses execute to sleep and then start a service via init.d
+ When I check the cookbook
+ Then the service resource warning 004 should be displayed
+
+ Scenario: Execute resource not controlling a service
+ Given a cookbook recipe that uses execute to list a directory
+ When I check the cookbook
+ Then the service resource warning 004 should not be displayed
145 features/step_definitions/cookbook_steps.rb
@@ -0,0 +1,145 @@
+Given /^a cookbook with a single recipe that accesses node attributes via strings$/ do
+ write_recipe %q{node['foo'] = 'bar'}
+end
+
+Given /^a cookbook with a single recipe that accesses multiple node attributes via strings$/ do
+ write_recipe %q{node['foo'] = 'bar'
+node['testing'] = 'bar'
+ }
+end
+
+Given /^a cookbook with a single recipe that assigns node attributes accessed via strings to a local variable$/ do
+ write_recipe %q{baz = node['foo']}
+end
+
+Given /^a cookbook with a single recipe that accesses nested node attributes via strings$/ do
+ write_recipe %q{node['foo']['foo2'] = 'bar'}
+end
+
+Given /^a cookbook with a single recipe that accesses node attributes via symbols/ do
+ write_recipe %q{node[:foo] = 'bar'}
+end
+
+Given /^a cookbook that declares ([a-z]+) attributes via strings$/ do |attribute_type|
+ write_attributes %Q{#{attribute_type}["apache"]["dir"] = "/etc/apache2"}
+end
+
+When /^I check the cookbook$/ do
+ run_lint
+end
+
+Then /^the (?:[a-z ]+) warning ([0-9]+) should be displayed( against the attributes file)?$/ do |code, atts|
+ expect_warning("FC#{code}", atts.nil? ? {} : {:file => 'attributes/default.rb'})
+end
+
+Then /^the node access warning 001 should be displayed for each match$/ do
+ expect_warning('FC001', :line => 1)
+ expect_warning('FC001', :line => 2)
+end
+
+Then /^the node access warning 001 should be displayed twice for the same line$/ do
+ expect_warning('FC001', :line => 1, :num_occurrences => 2)
+end
+
+Then /^the (?:[a-z ]+) warning ([0-9]+) should not be displayed$/ do |code|
+ expect_no_warning("FC#{code}")
+end
+
+Given /^a cookbook with a single recipe that creates a directory resource with an interpolated name$/ do
+ write_recipe %q{
+ directory "#{node[:base_dir]}" do
+ owner "root"
+ group "root"
+ mode "0755"
+ action :create
+ end
+ }.strip
+end
+
+Given /^a cookbook with a single recipe that creates a directory resource with an interpolated name from a string$/ do
+ write_recipe %q{
+ directory "#{node['base_dir']}" do
+ owner "root"
+ group "root"
+ mode "0755"
+ action :create
+ end
+ }.strip
+end
+
+Given /^a cookbook with a single recipe that creates a directory resource with a string literal$/ do
+ write_recipe %q{
+ directory "/var/lib/foo" do
+ owner "root"
+ group "root"
+ mode "0755"
+ action :create
+ end
+ }.strip
+end
+
+Given /^a cookbook with a single recipe that creates a directory resource with a compound expression$/ do
+ write_recipe %q{
+ directory "#{node[:base_dir]}#{node[:sub_dir]}" do
+ owner "root"
+ group "root"
+ mode "0755"
+ action :create
+ end
+ }.strip
+end
+
+Given /^a cookbook with a single recipe that creates a directory resource with an interpolated literal and expression$/ do
+ write_recipe %q{
+ directory "#{node[:base_dir]}/sub_dir" do
+ owner "root"
+ group "root"
+ mode "0755"
+ action :create
+ end
+ }.strip
+end
+
+Given /^a cookbook with a single recipe that searches without checking if this is server$/ do
+ write_recipe %q{nodes = search(:node, "hostname:[* TO *] AND chef_environment:#{node.chef_environment}")}
+end
+
+Given /^a cookbook with a single recipe that searches but checks first to see if this is server$/ do
+ write_recipe %q{
+ if Chef::Config[:solo]
+ Chef::Log.warn("This recipe uses search. Chef Solo does not support search.")
+ else
+ nodes = search(:node, "hostname:[* TO *] AND chef_environment:#{node.chef_environment}")
+ end
+ }.strip
+end
+
+Then /^the check for server warning 003 should not be displayed given we have checked$/ do
+ expect_warning("FC004", :line => 4, :expect_warning => false)
+end
+
+Given /^a cookbook recipe that uses execute to (sleep and then )?start a service via (.*)$/ do |sleep, method|
+ cmd = case
+ when method.include?('init.d')
+ '/etc/init.d/foo start'
+ when method.include?('full path')
+ '/sbin/service foo start'
+ else
+ 'service foo start'
+ end
+ write_recipe %Q{
+ execute "start-foo-service" do
+ command "#{sleep.nil? ? '' : 'sleep 5; '}#{cmd}"
+ action :run
+ end
+ }.strip
+end
+
+Given /^a cookbook recipe that uses execute to list a directory$/ do
+ write_recipe %Q{
+ execute "nothing-to-see-here" do
+ command "ls"
+ action :run
+ end
+ }.strip
+end
5 features/support/env.rb
@@ -0,0 +1,5 @@
+require 'simplecov'
+SimpleCov.start
+
+require 'aruba/cucumber'
+
44 features/support/lint_helpers.rb
@@ -0,0 +1,44 @@
+module FoodCritic
+
+ module Helpers
+
+ def write_recipe(content)
+ write_file 'cookbooks/example/recipes/default.rb', content
+ end
+
+ def write_attributes(content)
+ write_file 'cookbooks/example/attributes/default.rb', content
+ end
+
+ def run_lint
+ run_simple(unescape('foodcritic cookbooks/example/'), false)
+ end
+
+ def expect_warning(code, options={})
+ opt = {:line => 1, :expect_warning => true, :file => 'recipes/default.rb'}.merge!(options)
+ warning_text = case code
+ when 'FC001' then
+ 'Use symbols in preference to strings to access node attributes'
+ when 'FC002' then
+ 'Avoid string interpolation where not required'
+ when 'FC003' then
+ 'Check whether you are running with chef server before using server-specific features'
+ when 'FC004' then
+ 'Use a service resource to start and stop services'
+ end
+
+ if opt[:expect_warning]
+ assert_partial_output("#{code}: #{warning_text}: #{opt[:file]}:#{opt[:line]}\n", all_output)
+ else
+ assert_no_partial_output("#{code}: #{warning_text}: #{opt[:file]}:#{opt[:line]}\n", all_output)
+ end
+ end
+
+ def expect_no_warning(code, options={:expect_warning => false})
+ expect_warning(code, options)
+ end
+ end
+
+end
+
+World(FoodCritic::Helpers)
15 foodcritic.gemspec
@@ -0,0 +1,15 @@
+lib = File.expand_path('../lib/', __FILE__)
+$:.unshift lib unless $:.include?(lib)
+require 'foodcritic/version'
+Gem::Specification.new do |s|
+ s.name = 'foodcritic'
+ s.version = FoodCritic::VERSION
+ s.description = 'Lint tool for Opscode Chef cookbooks.'
+ s.summary = "foodcritic-#{s.version}"
+ s.authors = ['Andrew Crump']
+ s.homepage = 'http://acrmp.github.com/foodcritic'
+ s.license = 'MIT'
+ s.executables << 'foodcritic'
+ s.files = Dir['lib/**/*.rb']
+ s.required_ruby_version = '>= 1.9.3'
+end
5 lib/foodcritic.rb
@@ -0,0 +1,5 @@
+require 'foodcritic/domain'
+require 'foodcritic/helpers'
+require 'foodcritic/dsl'
+require 'foodcritic/linter'
+require 'foodcritic/version'
50 lib/foodcritic/domain.rb
@@ -0,0 +1,50 @@
+module FoodCritic
+
+ # A warning of a possible issue
+ class Warning
+ attr_reader :rule, :match
+
+ # Create a new warning
+ #
+ # @param [FoodCritic::Rule] rule The rule which raised this warning
+ # @param [Hash] match The match data
+ # @option match [String] :filename The filename the warning was raised against
+ # @option match [Integer] :line The identified line
+ # @option match [Integer] :column The identified column
+ def initialize(rule, match={})
+ @rule, @match = rule, match
+ end
+ end
+
+ # The collected warnings (if any) raised against a cookbook tree.
+ class Review
+
+ # Create a new review
+ #
+ # @param [Array] warnings The warnings raised in this review
+ def initialize(warnings)
+ @warnings = warnings
+ end
+
+ # Returns a string representation of this review.
+ #
+ # @return [String] Review as a string, this representation is liable to change.
+ def to_s
+ @warnings.map { |w| "#{w.rule.code}: #{w.rule.name}: #{w.match[:filename]}:#{w.match[:line]}" }.sort.uniq.join("\n")
+ end
+ end
+
+ # A rule to be matched against.
+ class Rule
+ attr_accessor :code, :name, :description, :recipe
+
+ # Create a new rule
+ #
+ # @param [String] code The short unique identifier for this rule, e.g. 'FC001'
+ # @param [String] name The short descriptive name of this rule presented to the end user.
+ def initialize(code, name)
+ @code, @name = code, name
+ end
+ end
+
+end
44 lib/foodcritic/dsl.rb
@@ -0,0 +1,44 @@
+module FoodCritic
+
+ # The DSL methods exposed for defining rules.
+ class RuleDsl
+ attr_reader :rules
+ include Helpers
+
+ # Define a new rule
+ #
+ # @param [String] code The short unique identifier for this rule, e.g. 'FC001'
+ # @param [String] name The short descriptive name of this rule presented to the end user.
+ # @param [Block] block The rule definition
+ def rule(code, name, &block)
+ @rules = [] if @rules.nil?
+ @rules << Rule.new(code, name)
+ yield self
+ end
+
+ # Set the rule description
+ #
+ # @param [String] Set the rule description.
+ def description(description)
+ rules.last.description = description
+ end
+
+ # Define a matcher that will be passed the AST with this method.
+ #
+ # @param [block] Your implemented matcher that returns a match Hash.
+ def recipe(&block)
+ rules.last.recipe = block
+ end
+
+ # Load the ruleset
+ #
+ # @param [String] filename The path to the ruleset to load
+ # @return [Array] The loaded rules, ready to be matched against provided cookbooks.
+ def self.load(filename)
+ dsl = RuleDsl.new
+ dsl.instance_eval(File.read(filename), filename)
+ dsl.rules
+ end
+ end
+
+end
56 lib/foodcritic/helpers.rb
@@ -0,0 +1,56 @@
+module FoodCritic
+
+ # Helper methods that form part of the Rules DSL.
+ module Helpers
+
+ # Given an AST type and parsed tree, return the matching subset.
+ #
+ # @param [Symbol] type The type of AST node to look for
+ # @param [Array] node The parsed AST (or part there-of)
+ # @return [Array] Matching nodes
+ def ast(type, node)
+ result = []
+ result = [node] if node.first == type
+ node.each { |node| result += ast(type, node) if node.respond_to?(:each) }
+ result
+ end
+
+ # Does the specified recipe check for Chef Solo?
+ #
+ # @param [Array] ast The AST of the cookbook recipe to check.
+ # @return [Boolean] True if there is a test for Chef::Config[:solo] in the recipe
+ def checks_for_chef_solo?(ast)
+ arefs = self.ast(:aref, ast)
+ arefs.any? do |aref|
+ self.ast(:@const, aref).map { |const| const[1] } == ['Chef', 'Config'] and
+ self.ast(:@ident, self.ast(:symbol, aref)).map { |sym| sym.drop(1).first }.include? 'solo'
+ end
+ end
+
+ # Find Chef resources of the specified type
+ #
+ # @param [Array] ast The AST of the cookbook recipe to check
+ # @param [String] type The type of resource to look for
+ def find_resources(ast, type)
+ self.ast(:method_add_block, ast).find_all do |resource|
+ resource[1][0] == :command and resource[1][1][0] == :@ident and resource[1][1][1] == type
+ end
+ end
+
+ # Retrieve a single-valued attribute from the specified resource.
+ #
+ # @param name The attribute name
+ # @param resource The resource AST to lookup the attribute under
+ # @return [String] The attribute value for the specified attribute
+ def resource_attribute(name, resource)
+ cmd = self.ast(:command, self.ast(:do_block, resource))
+ atts = cmd.find_all { |att| ast(:@ident, att).flatten.drop(1).first == name }
+ value = self.ast(:@tstring_content, atts).flatten.drop(1)
+ unless value.empty?
+ return value.first
+ end
+ nil
+ end
+ end
+
+end
47 lib/foodcritic/linter.rb
@@ -0,0 +1,47 @@
+require 'ripper'
+
+module FoodCritic
+
+ # The main entry point for linting your Chef cookbooks.
+ class Linter
+
+ # Create a new Linter, loading any defined rules.
+ def initialize
+ load_rules
+ end
+
+ # Review the cookbooks at the provided path, identifying potential improvements.
+ #
+ # @param [String] cookbook_path The file path to an individual cookbook directory
+ # @return [FoodCritic::Review] A review of your cookbooks, with any warnings issued.
+ def check(cookbook_path)
+ warnings = []
+ files_to_process(cookbook_path).each do |file|
+ ast = Ripper::SexpBuilder.new(IO.read(file)).parse
+ @rules.each do |rule|
+ rule.recipe.yield(ast).each do |match|
+ warnings << Warning.new(rule, match.merge({:filename => file.gsub(/^#{Regexp.escape(ARGV[0])}/, '')}))
+ end
+ end
+ end
+ Review.new(warnings)
+ end
+
+ private
+
+ # Load the rules from the (fairly unnecessary) DSL.
+ def load_rules
+ @rules = RuleDsl.load(File.join(File.dirname(__FILE__), 'rules.rb'))
+ end
+
+ # Return the files within a cookbook tree that we are interested in trying to match rules against.
+ #
+ # @param [String] dir The cookbook directory
+ # @return [Array] The files underneath the provided directory to be processed.
+ def files_to_process(dir)
+ return [dir] unless File.directory? dir
+ Dir.glob(File.join(dir, '{attributes,recipes}/*.rb'))
+ end
+
+ end
+end
65 lib/foodcritic/rules.rb
@@ -0,0 +1,65 @@
+rule "FC001", "Use symbols in preference to strings to access node attributes" do
+ description "When accessing node attributes you should use a symbol for a key rather than a string literal."
+ recipe do |ast|
+ matches = []
+ attribute_refs = %w{node default override set normal}
+ aref_fields = self.ast(:aref, ast) + self.ast(:aref_field, ast)
+ aref_fields.each do |field|
+ is_node_aref = attribute_refs.include? self.ast(:@ident, field).flatten.drop(1).first
+ if is_node_aref
+ literal_strings = self.ast(:@tstring_content, field)
+ literal_strings.each do |str|
+ matches << {:matched => str[1], :line => str[2].first, :column => str[2].last}
+ end
+ end
+ end
+ matches
+ end
+end
+
+rule "FC002", "Avoid string interpolation where not required" do
+ description "When setting a resource value avoid string interpolation where not required."
+ recipe do |ast|
+ matches = []
+ self.ast(:string_literal, ast).each do |literal|
+ embed_expr = self.ast(:string_embexpr, literal)
+ if embed_expr.size == 1
+ literal[1].reject! { |expr| expr == embed_expr.first }
+ if self.ast(:@tstring_content, literal).empty?
+ self.ast(:@ident, embed_expr).map { |ident| ident.flatten.drop(1) }.each do |ident|
+ matches << {:matched => ident[0], :line => ident[1], :column => ident[2]}
+ end
+ end
+ end
+ end
+ matches
+ end
+end
+
+rule "FC003", "Check whether you are running with chef server before using server-specific features" do
+ description "Ideally your cookbooks should be usable without requiring chef server."
+ recipe do |ast|
+ matches = []
+ function_calls = self.ast(:@ident, self.ast(:fcall, ast)).map { |fcall| fcall.drop(1).flatten }
+ searches = function_calls.find_all { |fcall| fcall.first == 'search' }
+ unless searches.empty? || checks_for_chef_solo?(ast)
+ searches.each { |s| matches << {:matched => s[0], :line => s[1], :column => s[2]} }
+ end
+ matches
+ end
+end
+
+rule "FC004", "Use a service resource to start and stop services" do
+ description "Avoid use of execute to control services - use the service resource instead."
+ recipe do |ast|
+ matches = []
+ find_resources(ast, 'execute').find_all do |cmd|
+ cmd_str = resource_attribute('command', cmd)
+ cmd_str.include?('/etc/init.d') || cmd_str.start_with?('service ') || cmd_str.start_with?('/sbin/service ')
+ end.each do |service_cmd|
+ exec = ast(:@ident, service_cmd).first.drop(1).flatten
+ matches << {:matched => exec[0], :line => exec[1], :column => exec[2]}
+ end
+ matches
+ end
+end
3  lib/foodcritic/version.rb
@@ -0,0 +1,3 @@
+module FoodCritic
+ VERSION = '0.1.0'
+end
Please sign in to comment.
Something went wrong with that request. Please try again.