]> git.nbdom.net Git - nb.git/commitdiff
puppet-lint
authorNicolas Boisselier <nicolas.boisselier@gmail.com>
Mon, 23 Feb 2015 23:15:06 +0000 (23:15 +0000)
committerNicolas Boisselier <nicolas.boisselier@gmail.com>
Mon, 23 Feb 2015 23:15:06 +0000 (23:15 +0000)
12 files changed:
etc/bashrc
lib/puppet-lint.rb [new file with mode: 0644]
lib/puppet-lint/configuration.rb [new file with mode: 0644]
lib/puppet-lint/plugin.rb [new file with mode: 0644]
lib/puppet-lint/plugins.rb [new file with mode: 0644]
lib/puppet-lint/plugins/check_classes.rb [new file with mode: 0644]
lib/puppet-lint/plugins/check_conditionals.rb [new file with mode: 0644]
lib/puppet-lint/plugins/check_resources.rb [new file with mode: 0644]
lib/puppet-lint/plugins/check_strings.rb [new file with mode: 0644]
lib/puppet-lint/plugins/check_variables.rb [new file with mode: 0644]
lib/puppet-lint/plugins/check_whitespace.rb [new file with mode: 0644]
lib/puppet-lint/tasks/puppet-lint.rb [new file with mode: 0644]

index 45a7ec95785c76e7f01e11cd248eab6c79a2fb4d..e9a800716970356aa49d4fb427d9b28a76da4475 100644 (file)
@@ -3,16 +3,13 @@
 # ENVS
 #
 #################################################################################
-[ -z "$HOSTNAME" ] && HOSTNAME=`hostname`
-[ -z "$UID" ] && UID=`id -u`
-[ -z "$USER" ] && USER=`whoami`
-
 . "${BASH_SOURCE%/*}/bashrc.function"
 
 NB_ROOT=$(realpath ${BASH_SOURCE%/*}/..)
 [ -z "$HOME" ] && HOME=`realpath ~/`
-
-. "${BASH_SOURCE%/*}/bashrc.alias"
+[ -z "$HOSTNAME" ] && HOSTNAME=`hostname`
+[ -z "$UID" ] && UID=`id -u`
+[ -z "$USER" ] && USER=`whoami`
 
 #
 # PATH
@@ -36,6 +33,10 @@ PATH=`env-add-path "$PATH" \
 `
 export PATH
 
+export RUBYLIB=`env-add-path "$RUBYLIB" $NB_ROOT/lib`
+export PERL5LIB=`env-add-path "$PERL5LIB" $NB_ROOT/lib`
+export PYTHONPATH=`env-add-path "$PYTHONPATH" $NB_ROOT/lib`
+
 #
 # OTHERS
 #
@@ -45,7 +46,7 @@ export LESS="-iMR"
 export EDITOR=vim
 
 #
-# Color / PS1 / ls
+# Color
 #
 declare color_prompt color char h
 case "$TERM" in
@@ -54,6 +55,9 @@ case "$TERM" in
        linux) color_prompt=yes;;
 esac
 
+#
+# PS1
+#
 h='\h'
 #case $(tr '[:upper:]' '[:lower:]' <<<"$HOSTNAME") in
 case "$HOSTNAME" in
@@ -73,7 +77,9 @@ else
        PS1="\u@\h:\W${char} "
 fi
 
+#
 # ls
+#
 ls_opt='-a'
 case "$OSTYPE" in 
        darwin*) 
@@ -89,3 +95,7 @@ unset ls_opt
 alias ll='ls -lh'
 unset color char color_prompt h
 
+#
+# ALIASES
+#
+. "${BASH_SOURCE%/*}/bashrc.alias"
diff --git a/lib/puppet-lint.rb b/lib/puppet-lint.rb
new file mode 100644 (file)
index 0000000..01bb45d
--- /dev/null
@@ -0,0 +1,169 @@
+# We're doing this instead of a gem dependency so folks using Puppet
+# from their distro packages don't have to install the gem.
+begin
+  require 'puppet'
+rescue LoadError
+  puts 'Unable to require puppet.  Please gem install puppet and try again.'
+  exit 1
+end
+
+require 'puppet-lint/configuration'
+require 'puppet-lint/plugin'
+
+unless String.respond_to?('prepend')
+  class String
+    def prepend(lead)
+      self.replace "#{lead}#{self}"
+    end
+  end
+end
+
+# If we are using an older ruby version, we back-port the basic functionality
+# we need for formatting output: 'somestring' % <hash>
+begin
+  if ('%{test}' % {:test => 'replaced'} == 'replaced')
+    # If this works, we are all good to go.
+  end
+rescue
+  # If the test failed (threw a error), monkeypatch String.
+  # Most of this code came from http://www.ruby-forum.com/topic/144310 but was
+  # simplified for our use.
+
+  # Basic implementation of 'string' % { } like we need it. needs work.
+  class String
+    Percent = instance_method '%' unless defined? Percent
+    def % *a, &b
+      a.flatten!
+
+      string = case a.last
+      when Hash
+        expand a.pop
+      else
+        self
+      end
+
+      if a.empty?
+        string
+      else
+        Percent.bind(string).call(*a, &b)
+      end
+
+    end
+    def expand! vars = {}
+      loop do
+        changed = false
+        vars.each do |var, value|
+          var = var.to_s
+          var.gsub! %r/[^a-zA-Z0-9_]/, ''
+          [
+            %r/\%\{#{ var }\}/,
+          ].each do |pat|
+            changed = gsub! pat, "#{ value }"
+          end
+        end
+        break unless changed
+      end
+      self
+    end
+    def expand opts = {}
+      dup.expand! opts
+    end
+  end
+end
+
+class PuppetLint::NoCodeError < StandardError; end
+
+class PuppetLint
+  VERSION = '0.1.12'
+
+  attr_reader :code, :file
+
+  def initialize
+    @data = nil
+    @statistics = {:error => 0, :warning => 0}
+    @fileinfo = {:path => ''}
+  end
+
+  def self.configuration
+    @configuration ||= PuppetLint::Configuration.new
+  end
+
+  def configuration
+    self.class.configuration
+  end
+
+  def file=(path)
+    if File.exist? path
+      @fileinfo[:path] = path
+      @fileinfo[:fullpath] = File.expand_path(path)
+      @fileinfo[:filename] = File.basename(path)
+      @data = File.read(path)
+    end
+  end
+
+  def code=(value)
+    @data = value
+  end
+
+  def log_format
+    if configuration.log_format == ''
+      ## recreate previous old log format as far as thats possible.
+      format = '%{KIND}: %{message} on line %{linenumber}'
+      if configuration.with_filename
+        format.prepend '%{path} - '
+      end
+      configuration.log_format = format
+    end
+    return configuration.log_format
+  end
+
+  def format_message(message)
+    format = log_format
+    puts format % message
+  end
+
+  def report(problems)
+    problems.each do |message|
+      @statistics[message[:kind]] += 1
+      ## Add some default attributes.
+      message.merge!(@fileinfo) {|key, v1, v2| v1 }
+      message[:KIND] = message[:kind].to_s.upcase
+
+      if configuration.error_level == message[:kind] or configuration.error_level == :all
+        format_message message
+      end
+    end
+  end
+  
+  def errors?
+    @statistics[:error] != 0
+  end
+
+  def warnings?
+    @statistics[:warning] != 0
+  end
+
+  def checks
+    PuppetLint::CheckPlugin.repository.map do |plugin|
+      plugin.new.checks
+    end.flatten
+  end
+
+  def run
+    if @data.nil?
+      raise PuppetLint::NoCodeError
+    end
+
+    PuppetLint::CheckPlugin.repository.each do |plugin|
+      report plugin.new.run(@fileinfo, @data)
+    end
+  end
+end
+
+# Default configuration options
+PuppetLint.configuration.fail_on_warnings = false
+PuppetLint.configuration.error_level = :all
+PuppetLint.configuration.with_filename = false
+PuppetLint.configuration.log_format = ''
+
+require 'puppet-lint/plugins'
diff --git a/lib/puppet-lint/configuration.rb b/lib/puppet-lint/configuration.rb
new file mode 100644 (file)
index 0000000..970cc86
--- /dev/null
@@ -0,0 +1,57 @@
+class PuppetLint
+  class Configuration
+    def self.add_check(check)
+      define_method("#{check}_enabled?") do
+        settings["#{check}_disabled"] == true ? false : true
+      end
+
+      define_method("disable_#{check}") do
+        settings["#{check}_disabled"] = true
+      end
+
+      define_method("enable_#{check}") do
+        settings["#{check}_disabled"] = false
+      end
+    end
+
+    def method_missing(method, *args, &block)
+      if method.to_s =~ /^(\w+)=$/
+        option = $1
+        add_option(option.to_s) if settings[option].nil?
+        settings[option] = args[0]
+      else
+        nil
+      end
+    end
+
+    def add_option(option)
+      self.class.add_option(option)
+    end
+
+    def self.add_option(option)
+      define_method("#{option}=") do |value|
+        settings[option] = value
+      end
+
+      define_method(option) do
+        settings[option]
+      end
+    end
+
+    def add_check(check)
+      self.class.add_check(check)
+    end
+
+    def settings
+      @settings ||= {}
+    end
+
+    def checks
+      self.public_methods.select { |method|
+        method =~ /^.+_enabled\?$/
+      }.map { |method|
+        method[0..-10]
+      }
+    end
+  end
+end
diff --git a/lib/puppet-lint/plugin.rb b/lib/puppet-lint/plugin.rb
new file mode 100644 (file)
index 0000000..bd6796c
--- /dev/null
@@ -0,0 +1,162 @@
+class PuppetLint
+
+  module Plugin
+    module ClassMethods
+      def repository
+        @repository ||= []
+      end
+
+      def inherited(klass)
+        repository << klass
+      end
+    end
+
+    def self.included(klass)
+      klass.extend ClassMethods
+    end
+  end
+end
+
+class PuppetLint::CheckPlugin
+  include PuppetLint::Plugin
+  attr_reader :problems, :checks
+
+  def initialize
+    @problems = []
+    @checks = []
+    @default_info = {:check => 'unknown', :linenumber => 0}
+  end
+
+  def register_check(check)
+    @checks << check
+  end
+
+  #     notify(kind, message_hash)    #=> nil
+  #
+  # Adds the message to the problems array.
+  # The _kind_ gets added to the _message_hash_ by setting the key :_kind_.
+  # Typically, the _message_hash_ should contain following keys:
+  # <i>message</i>::     which contains a string value describing the problem
+  # <i>linenumber</i>::  which contains the line number on which the problem occurs.
+  # Besides the :_kind_ value that is being set, some other key/values are also
+  # added. Typically, this is
+  # <i>check</i>::      which contains the name of the check that is being executed.
+  # <i>linenumber</i>:: which defaults to 0 if the message does not already contain one.
+  #
+  #     notify :warning, :message => "Something happened", :linenumber => 4
+  #     => {:kind=>:warning, :message=>"Something happened", :linenumber=>4, :check=>'unknown'}
+  #
+  def notify(kind, message_hash)
+    message_hash[:kind] = kind
+    message_hash.merge!(@default_info) {|key, v1, v2| v1 }
+    @problems << message_hash
+    message_hash
+  end
+
+  def run(fileinfo, data)
+    lexer = Puppet::Parser::Lexer.new
+    lexer.string = data
+    @tokens = lexer.fullscan
+    @fileinfo = fileinfo
+    @data = data
+
+    self.public_methods.select { |method|
+      method.to_s.start_with? 'lint_check_'
+    }.each { |method|
+      name = method.to_s[11..-1]
+      @default_info[:check] = name
+      self.send(method) if PuppetLint.configuration.send("#{name}_enabled?")
+    }
+
+    @problems
+  end
+
+  def filter_tokens
+    @title_tokens = []
+    @resource_indexes = []
+    @class_indexes = []
+    @defined_type_indexes = []
+
+    @tokens.each_index do |token_idx|
+      if @tokens[token_idx].first == :COLON
+        # gather a list of tokens that are resource titles
+        if @tokens[token_idx-1].first == :RBRACK
+          title_array_tokens = @tokens[@tokens.rindex { |r| r.first == :LBRACK }+1..token_idx-2]
+          @title_tokens += title_array_tokens.select { |token| [:STRING, :NAME].include? token.first }
+        else
+          if @tokens[token_idx + 1].first != :LBRACE
+            @title_tokens << @tokens[token_idx-1]
+          end
+        end
+
+        # gather a list of start and end indexes for resource attribute blocks
+        if @tokens[token_idx+1].first != :LBRACE
+          @resource_indexes << {:start => token_idx+1, :end => @tokens[token_idx+1..-1].index { |r| [:SEMIC, :RBRACE].include? r.first }+token_idx}
+        end
+      elsif [:CLASS, :DEFINE].include? @tokens[token_idx].first
+        lbrace_count = 0
+        @tokens[token_idx+1..-1].each_index do |class_token_idx|
+          idx = class_token_idx + token_idx
+          if @tokens[idx].first == :LBRACE
+            lbrace_count += 1
+          elsif @tokens[idx].first == :RBRACE
+            lbrace_count -= 1
+            if lbrace_count == 0
+              if @tokens[token_idx].first == :CLASS and @tokens[token_idx + 1].first != :LBRACE
+                @class_indexes << {:start => token_idx, :end => idx}
+              end
+              @defined_type_indexes << {:start => token_idx, :end => idx} if @tokens[token_idx].first == :DEFINE
+              break
+            end
+          end
+        end
+      end
+    end
+  end
+
+  def tokens
+    @tokens
+  end
+
+  def path
+    @fileinfo[:path]
+  end
+
+  def fullpath
+    @fileinfo[:fullpath]
+  end
+
+  def data
+    @data
+  end
+
+  def title_tokens
+    filter_tokens if @title_tokens.nil?
+    @title_tokens
+  end
+
+  def resource_indexes
+    filter_tokens if @resource_indexes.nil?
+    @resource_indexes
+  end
+
+  def class_indexes
+    filter_tokens if @class_indexes.nil?
+    @class_indexes
+  end
+
+  def defined_type_indexes
+    filter_tokens if @defined_type_indexes.nil?
+    @defined_type_indexes
+  end
+
+  def manifest_lines
+    @manifest_lines ||= @data.split("\n")
+  end
+
+  def self.check(name, &b)
+    PuppetLint.configuration.add_check name
+    define_method("lint_check_#{name}", b)
+  end
+end
+
diff --git a/lib/puppet-lint/plugins.rb b/lib/puppet-lint/plugins.rb
new file mode 100644 (file)
index 0000000..a0e0d0f
--- /dev/null
@@ -0,0 +1,11 @@
+class PuppetLint
+  class Plugins
+  end
+end
+
+require 'puppet-lint/plugins/check_classes'
+require 'puppet-lint/plugins/check_conditionals'
+require 'puppet-lint/plugins/check_strings'
+require 'puppet-lint/plugins/check_variables'
+require 'puppet-lint/plugins/check_whitespace'
+require 'puppet-lint/plugins/check_resources'
diff --git a/lib/puppet-lint/plugins/check_classes.rb b/lib/puppet-lint/plugins/check_classes.rb
new file mode 100644 (file)
index 0000000..afecfdf
--- /dev/null
@@ -0,0 +1,137 @@
+class PuppetLint::Plugins::CheckClasses < PuppetLint::CheckPlugin
+  if Puppet::PUPPETVERSION !~ /^0\.2/
+    check 'right_to_left_relationship' do
+      tokens.select { |r| r.first == :OUT_EDGE }.each do |token|
+        notify :warning, :message =>  "right-to-left (<-) relationship", :linenumber => token.last[:line]
+      end
+    end
+  end
+
+  check 'autoloader_layout' do
+    unless fullpath == ""
+      (class_indexes + defined_type_indexes).each do |class_idx|
+        title_token = tokens[class_idx[:start]+1]
+        split_title = title_token.last[:value].split('::')
+        if split_title.length > 1
+          expected_path = "#{split_title.first}/manifests/#{split_title[1..-1].join('/')}.pp"
+        else
+          expected_path = "#{title_token.last[:value]}/manifests/init.pp"
+        end
+
+        unless fullpath.end_with? expected_path
+          notify :error, :message =>  "#{title_token.last[:value]} not in autoload module layout", :linenumber => title_token.last[:line]
+        end
+      end
+    end
+  end
+
+  check 'parameter_order' do
+    (class_indexes + defined_type_indexes).each do |class_idx|
+      token_idx = class_idx[:start]
+      header_end_idx = tokens[token_idx..-1].index { |r| r.first == :LBRACE }
+      lparen_idx = tokens[token_idx..(header_end_idx + token_idx)].index { |r| r.first == :LPAREN }
+      rparen_idx = tokens[token_idx..(header_end_idx + token_idx)].rindex { |r| r.first == :RPAREN }
+
+      unless lparen_idx.nil? or rparen_idx.nil?
+        param_tokens = tokens[lparen_idx..rparen_idx]
+        param_tokens.each_index do |param_tokens_idx|
+          this_token = param_tokens[param_tokens_idx]
+          next_token = param_tokens[param_tokens_idx+1]
+          prev_token = param_tokens[param_tokens_idx-1]
+          if this_token.first == :VARIABLE
+            unless next_token.nil?
+              if next_token.first == :COMMA or next_token.first == :RPAREN
+                unless param_tokens[0..param_tokens_idx].rindex { |r| r.first == :EQUALS }.nil?
+                  unless prev_token.nil? or prev_token.first == :EQUALS
+                    notify :warning, :message =>  "optional parameter listed before required parameter", :linenumber => this_token.last[:line]
+                  end
+                end
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+
+  check 'inherits_across_namespaces' do
+    class_indexes.each do |class_idx|
+      token_idx = class_idx[:start]
+      if tokens[token_idx+2].first == :INHERITS
+        class_name = tokens[token_idx+1].last[:value]
+        inherited_class = tokens[token_idx+3].last[:value]
+
+        unless class_name =~ /^#{inherited_class}::/
+          notify :warning, :message =>  "class inherits across namespaces", :linenumber => tokens[token_idx].last[:line]
+        end
+      end
+    end
+  end
+
+  check 'nested_classes_or_defines' do
+    class_indexes.each do |class_idx|
+      class_tokens = tokens[class_idx[:start]..class_idx[:end]]
+      class_tokens[1..-1].each_index do |token_idx|
+        token = class_tokens[1..-1][token_idx]
+        next_token = class_tokens[1..-1][token_idx + 1]
+
+        if token.first == :CLASS
+          if next_token.first != :LBRACE
+            notify :warning, :message =>  "class defined inside a class", :linenumber => token.last[:line]
+          end
+        end
+
+        if token.first == :DEFINE
+          notify :warning, :message =>  "define defined inside a class", :linenumber => token.last[:line]
+        end
+      end
+    end
+  end
+
+  check 'variable_scope' do
+    (class_indexes + defined_type_indexes).each do |idx|
+      object_tokens = tokens[idx[:start]..idx[:end]]
+      variables_in_scope = ['name', 'title', 'module_name', 'environment', 'clientcert', 'clientversion', 'servername', 'serverip', 'serverversion', 'caller_module_name']
+      referenced_variables = []
+      header_end_idx = object_tokens.index { |r| r.first == :LBRACE }
+      lparen_idx = object_tokens[0..header_end_idx].index { |r| r.first == :LPAREN }
+      rparen_idx = object_tokens[0..header_end_idx].rindex { |r| r.first == :RPAREN }
+
+      unless lparen_idx.nil? or rparen_idx.nil?
+        param_tokens = object_tokens[lparen_idx..rparen_idx]
+        param_tokens.each_index do |param_tokens_idx|
+          this_token = param_tokens[param_tokens_idx]
+          next_token = param_tokens[param_tokens_idx+1]
+          if this_token.first == :VARIABLE
+            if [:COMMA, :EQUALS, :RPAREN].include? next_token.first
+              variables_in_scope << this_token.last[:value]
+            end
+          end
+        end
+      end
+
+      object_tokens.each_index do |object_token_idx|
+        this_token = object_tokens[object_token_idx]
+        next_token = object_tokens[object_token_idx + 1]
+
+        if this_token.first == :VARIABLE
+          if next_token.first == :EQUALS
+            variables_in_scope << this_token.last[:value]
+          else
+            referenced_variables << this_token
+          end
+        end
+      end
+
+      referenced_variables.each do |token|
+        unless token.last[:value].include? '::'
+          unless variables_in_scope.include? token.last[:value]
+            unless token.last[:value] =~ /\d+/
+              notify :warning, :message =>  "top-scope variable being used without an explicit namespace", :linenumber => token.last[:line]
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/puppet-lint/plugins/check_conditionals.rb b/lib/puppet-lint/plugins/check_conditionals.rb
new file mode 100644 (file)
index 0000000..3518e2f
--- /dev/null
@@ -0,0 +1,56 @@
+class PuppetLint::Plugins::CheckConditionals < PuppetLint::CheckPlugin
+  check 'selector_inside_resource' do
+    resource_indexes.each do |resource|
+      resource_tokens = tokens[resource[:start]..resource[:end]]
+
+      resource_tokens.each_index do |resource_token_idx|
+        if resource_tokens[resource_token_idx].first == :FARROW
+          if resource_tokens[resource_token_idx + 1].first == :VARIABLE
+            unless resource_tokens[resource_token_idx + 2].nil?
+              if resource_tokens[resource_token_idx + 2].first == :QMARK
+                notify :warning, :message =>  "selector inside resource block", :linenumber => resource_tokens[resource_token_idx].last[:line]
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+
+  check 'case_without_default' do
+    case_indexes = []
+
+    tokens.each_index do |token_idx|
+      if tokens[token_idx].first == :COLON
+        # gather a list of start and end indexes for resource attribute blocks
+        if tokens[token_idx+1].first != :LBRACE
+          resource_indexes << {:start => token_idx+1, :end => tokens[token_idx+1..-1].index { |r| [:SEMIC, :RBRACE].include? r.first }+token_idx}
+        end
+      end
+
+      if tokens[token_idx].first == :CASE
+        lbrace_count = 0
+        tokens[token_idx+1..-1].each_index do |case_token_idx|
+          idx = case_token_idx + token_idx
+          if tokens[idx].first == :LBRACE
+            lbrace_count += 1
+          elsif tokens[idx].first == :RBRACE
+            lbrace_count -= 1
+            if lbrace_count == 0
+              case_indexes << {:start => token_idx, :end => idx}
+              break
+            end
+          end
+        end
+      end
+    end
+
+    case_indexes.each do |kase|
+      case_tokens = tokens[kase[:start]..kase[:end]]
+
+      unless case_tokens.index { |r| r.first == :DEFAULT }
+        notify :warning, :message =>  "case statement without a default case", :linenumber => case_tokens.first.last[:line]
+      end
+    end
+  end
+end
diff --git a/lib/puppet-lint/plugins/check_resources.rb b/lib/puppet-lint/plugins/check_resources.rb
new file mode 100644 (file)
index 0000000..e39109a
--- /dev/null
@@ -0,0 +1,79 @@
+# Resources
+# http://docs.puppetlabs.com/guides/style_guide.html#resources
+
+class PuppetLint::Plugins::CheckResources < PuppetLint::CheckPlugin
+  check 'unquoted_resource_title' do
+    title_tokens.each do |token|
+      if token.first == :NAME
+        notify :warning, :message =>  "unquoted resource title", :linenumber => token.last[:line]
+      end
+    end
+  end
+
+  check 'ensure_first_param' do
+    resource_indexes.each do |resource|
+      resource_tokens = tokens[resource[:start]..resource[:end]]
+      ensure_attr_index = resource_tokens.index { |token| token.first == :NAME and token.last[:value] == 'ensure' }
+      unless ensure_attr_index.nil?
+        if ensure_attr_index > 1
+          ensure_attr_line_no = resource_tokens[ensure_attr_index].last[:line]
+          notify :warning, :message =>  "ensure found on line but it's not the first attribute", :linenumber => ensure_attr_line_no
+        end
+      end
+    end
+  end
+
+  check 'unquoted_file_mode' do
+    resource_indexes.each do |resource|
+      resource_tokens = tokens[resource[:start]..resource[:end]]
+      resource_type_token = tokens[tokens[0..resource[:start]].rindex { |r| r.first == :LBRACE } - 1]
+      if resource_type_token.last[:value] == "file"
+        resource_tokens.each_index do |resource_token_idx|
+          attr_token = resource_tokens[resource_token_idx]
+          if attr_token.first == :NAME and attr_token.last[:value] == 'mode'
+            value_token = resource_tokens[resource_token_idx + 2]
+            if value_token.first == :NAME
+              notify :warning, :message =>  "unquoted file mode", :linenumber => value_token.last[:line]
+            end
+          end
+        end
+      end
+    end
+  end
+
+  check 'file_mode' do
+    resource_indexes.each do |resource|
+      resource_tokens = tokens[resource[:start]..resource[:end]]
+      resource_type_token = tokens[tokens[0..resource[:start]].rindex { |r| r.first == :LBRACE } - 1]
+      if resource_type_token.last[:value] == "file"
+        resource_tokens.each_index do |resource_token_idx|
+          attr_token = resource_tokens[resource_token_idx]
+          if attr_token.first == :NAME and attr_token.last[:value] == 'mode'
+            value_token = resource_tokens[resource_token_idx + 2]
+            if value_token.last[:value] !~ /\d{4}/ and value_token.first != :VARIABLE and value_token.last[:value] !~ /^([ugoa]*[-=+][-=+rstwxXugo]*)(,[ugoa]*[-=+][-=+rstwxXugo]*)*$/
+              notify :warning, :message =>  "mode should be represented as a 4 digit octal value or symbolic file mode", :linenumber => value_token.last[:line]
+            end
+          end
+        end
+      end
+    end
+  end
+
+  check 'ensure_not_symlink_target' do
+    resource_indexes.each do |resource|
+      resource_tokens = tokens[resource[:start]..resource[:end]]
+      resource_type_token = tokens[tokens[0..resource[:start]].rindex { |r| r.first == :LBRACE } - 1]
+      if resource_type_token.last[:value] == "file"
+        resource_tokens.each_index do |resource_token_idx|
+          attr_token = resource_tokens[resource_token_idx]
+          if attr_token.first == :NAME and attr_token.last[:value] == 'ensure'
+            value_token = resource_tokens[resource_token_idx + 2]
+            if value_token.last[:value].start_with? '/'
+              notify :warning, :message =>  "symlink target specified in ensure attr", :linenumber => value_token.last[:line]
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/puppet-lint/plugins/check_strings.rb b/lib/puppet-lint/plugins/check_strings.rb
new file mode 100644 (file)
index 0000000..ead1ae1
--- /dev/null
@@ -0,0 +1,106 @@
+class PuppetLint::Plugins::CheckStrings < PuppetLint::CheckPlugin
+  class ::Puppet::Parser::Lexer
+    class TokenList
+      def del_token(token)
+        @tokens.delete(token)
+      end
+    end
+
+    TOKENS.add_tokens("<single quotes string>" => :SSTRING)
+    TOKENS.del_token(:SQUOTE)
+
+    if Puppet::PUPPETVERSION =~ /^0\.2/
+      TOKENS.add_token :SQUOTE, "'" do |lexer, value|
+        value = lexer.slurpstring(value)
+        [TOKENS[:SSTRING], value]
+      end
+    else
+      TOKENS.add_token :SQUOTE, "'" do |lexer, value|
+        [ TOKENS[:SSTRING], lexer.slurpstring(value,["'"],:ignore_invalid_escapes).first ]
+      end
+    end
+  end
+
+  check 'double_quoted_strings' do
+    tokens.each_index do |token_idx|
+      token = tokens[token_idx]
+
+      if token.first == :STRING
+        unless token.last[:value].include? "\t" or token.last[:value].include? "\n"
+          notify :warning, :message =>  "double quoted string containing no variables", :linenumber => token.last[:line]
+        end
+      elsif token.first == :DQTEXT
+        unless token.last[:value].include? "\\t" or token.last[:value].include? "\\n" or token.last[:value] =~ /[^\\]?\$\{?/
+          notify :warning, :message =>  "double quoted string containing no variables", :linenumber => token.last[:line]
+        end
+      end
+    end
+  end
+
+  check 'only_variable_string' do
+    tokens.each_index do |token_idx|
+      token = tokens[token_idx]
+
+      if token.first == :DQPRE and token.last[:value] == ""
+        if tokens[token_idx + 1].first == :VARIABLE
+          if tokens[token_idx + 2].first == :DQPOST and tokens[token_idx + 2].last[:value] == ""
+            notify :warning, :message =>  "string containing only a variable", :linenumber => tokens[token_idx + 1].last[:line]
+          end
+        end
+      end
+      if token.first == :DQTEXT and token.last[:value] =~ /\A\$\{.+\}\Z/
+        notify :warning, :message =>  "string containing only a variable", :linenumber => token.last[:line]
+      end
+    end
+  end
+
+  check 'variables_not_enclosed' do
+    tokens.each_index do |token_idx|
+      token = tokens[token_idx]
+
+      if token.first == :DQPRE
+        end_of_string_idx = tokens[token_idx..-1].index { |r| r.first == :DQPOST }
+        tokens[token_idx..end_of_string_idx].each do |t|
+          if t.first == :VARIABLE
+            line = data.split("\n")[t.last[:line] - 1]
+            if line.is_a? String and line.include? "$#{t.last[:value]}"
+              notify :warning, :message =>  "variable not enclosed in {}", :linenumber => t.last[:line]
+            end
+          end
+        end
+      elsif token.first == :DQTEXT and token.last[:value] =~ /\$\w+/
+        notify :warning, :message =>  "variable not enclosed in {}", :linenumber => token.last[:line]
+      end
+    end
+  end
+
+  check 'single_quote_string_with_variables' do
+    tokens.each_index do |token_idx|
+      token = tokens[token_idx]
+
+      if token.first == :SSTRING
+        contents = token.last[:value]
+        line_no = token.last[:line]
+
+        if contents.include? '${'
+          notify :error, :message =>  "single quoted string containing a variable found", :linenumber => token.last[:line]
+        end
+      end
+    end
+  end
+
+  check 'quoted_booleans' do
+    tokens.each_index do |token_idx|
+      token = tokens[token_idx]
+
+      if token.first == :SSTRING
+        contents = token.last[:value]
+        line_no = token.last[:line]
+
+        if ['true', 'false'].include? contents
+          notify :warning, :message =>  "quoted boolean value found", :linenumber => token.last[:line]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/puppet-lint/plugins/check_variables.rb b/lib/puppet-lint/plugins/check_variables.rb
new file mode 100644 (file)
index 0000000..8519e77
--- /dev/null
@@ -0,0 +1,24 @@
+class PuppetLint::Plugins::CheckVariables < PuppetLint::CheckPlugin
+  check 'variable_contains_dash' do
+    tokens.each_index do |token_idx|
+      token = tokens[token_idx]
+
+      if token.first == :VARIABLE
+        variable = token.last[:value]
+        line_no = token.last[:line]
+        if variable.match(/-/)
+          notify :warning, :message =>  "variable contains a dash", :linenumber => line_no
+        end
+      end
+
+      if token.first == :DQPRE
+        end_of_string_idx = tokens[token_idx..-1].index { |r| r.first == :DQPOST }
+        tokens[token_idx..end_of_string_idx].each do |t|
+          if t.first == :VARIABLE and t.last[:value].match(/-/)
+            notify :warning, :message =>  "variable contains a dash", :linenumber => t.last[:line]
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/puppet-lint/plugins/check_whitespace.rb b/lib/puppet-lint/plugins/check_whitespace.rb
new file mode 100644 (file)
index 0000000..6de0233
--- /dev/null
@@ -0,0 +1,103 @@
+# Spacing, Identation & Whitespace
+# http://docs.puppetlabs.com/guides/style_guide.html#spacing-indentation--whitespace
+
+class PuppetLint::Plugins::CheckWhitespace < PuppetLint::CheckPlugin
+  check 'hard_tabs' do
+    line_no = 0
+    manifest_lines.each do |line|
+      line_no += 1
+
+      # MUST NOT use literal tab characters
+      notify :error, :message =>  "tab character found", :linenumber => line_no if line.include? "\t"
+    end
+  end
+
+  check 'trailing_whitespace' do
+    line_no = 0
+    manifest_lines.each do |line|
+      line_no += 1
+
+      # MUST NOT contain trailing white space
+      notify :error, :message =>  "trailing whitespace found", :linenumber => line_no if line.end_with? " "
+    end
+  end
+
+  check '80chars' do
+    line_no = 0
+    manifest_lines.each do |line|
+      line_no += 1
+
+      # SHOULD NOT exceed an 80 character line width
+      unless line =~ /puppet:\/\//
+        notify :warning, :message =>  "line has more than 80 characters", :linenumber => line_no if line.length > 80
+      end
+    end
+  end
+
+  check '2sp_soft_tabs' do
+    line_no = 0
+    manifest_lines.each do |line|
+      line_no += 1
+
+      # MUST use two-space soft tabs
+      line.scan(/^ +/) do |prefix|
+        unless prefix.length % 2 == 0
+          notify :error, :message =>  "two-space soft tabs not used", :linenumber => line_no
+        end
+      end
+    end
+  end
+
+  check 'arrow_alignment' do
+    line_no = 0
+    in_resource = false
+    selectors = []
+    resource_indent_length = 0
+    manifest_lines.each do |line|
+      line_no += 1
+
+      # SHOULD align fat comma arrows (=>) within blocks of attributes
+      if line =~ /^( +.+? +)=>/
+        line_indent = $1
+        if in_resource
+          if selectors.count > 0
+            if selectors.last == 0
+              selectors[-1] = line_indent.length
+            end
+
+            # check for length first
+            unless line_indent.length == selectors.last
+              notify :warning, :message =>  "=> on line isn't properly aligned for selector", :linenumber => line_no
+            end
+
+            # then for a new selector or selector finish
+            if line.strip.end_with? "{"
+              selectors.push(0)
+            elsif line.strip =~ /\}[,;]?$/
+              selectors.pop
+            end
+          else
+            unless line_indent.length == resource_indent_length
+              notify :warning, :message =>  "=> on line isn't properly aligned for resource", :linenumber => line_no
+            end
+
+            if line.strip.end_with? "{"
+              selectors.push(0)
+            end
+          end
+        else
+          resource_indent_length = line_indent.length
+          in_resource = true
+          if line.strip.end_with? "{"
+            selectors.push(0)
+          end
+        end
+      elsif line.strip =~ /\}[,;]?$/ and selectors.count > 0
+        selectors.pop
+      else
+        in_resource = false
+        resource_indent_length = 0
+      end
+    end
+  end
+end
diff --git a/lib/puppet-lint/tasks/puppet-lint.rb b/lib/puppet-lint/tasks/puppet-lint.rb
new file mode 100644 (file)
index 0000000..ffd7a65
--- /dev/null
@@ -0,0 +1,25 @@
+require 'puppet-lint'
+require 'rake'
+require 'rake/tasklib'
+
+class PuppetLint
+  class RakeTask < ::Rake::TaskLib
+    def initialize(*args)
+      desc 'Run puppet-lint'
+
+      task :lint do
+        RakeFileUtils.send(:verbose, true) do
+          linter =  PuppetLint.new
+          Dir.glob('**/*.pp').each do |puppet_file|
+            puts "Evaluating #{puppet_file}"
+            linter.file = puppet_file
+            linter.run
+          end
+          fail if linter.errors?
+        end
+      end
+    end
+  end
+end
+
+PuppetLint::RakeTask.new