From: Nicolas Boisselier Date: Mon, 23 Feb 2015 23:15:06 +0000 (+0000) Subject: puppet-lint X-Git-Url: https://git.nbdom.net/?a=commitdiff_plain;h=9039a7dd4188006baac18a31685b6c51b46fb8eb;p=nb.git puppet-lint --- diff --git a/etc/bashrc b/etc/bashrc index 45a7ec95..e9a80071 100644 --- a/etc/bashrc +++ b/etc/bashrc @@ -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 index 00000000..01bb45d9 --- /dev/null +++ b/lib/puppet-lint.rb @@ -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' % +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 index 00000000..970cc863 --- /dev/null +++ b/lib/puppet-lint/configuration.rb @@ -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 index 00000000..bd6796c9 --- /dev/null +++ b/lib/puppet-lint/plugin.rb @@ -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: + # message:: which contains a string value describing the problem + # linenumber:: 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 + # check:: which contains the name of the check that is being executed. + # linenumber:: 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 index 00000000..a0e0d0f5 --- /dev/null +++ b/lib/puppet-lint/plugins.rb @@ -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 index 00000000..afecfdfe --- /dev/null +++ b/lib/puppet-lint/plugins/check_classes.rb @@ -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 index 00000000..3518e2f7 --- /dev/null +++ b/lib/puppet-lint/plugins/check_conditionals.rb @@ -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 index 00000000..e39109a3 --- /dev/null +++ b/lib/puppet-lint/plugins/check_resources.rb @@ -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 index 00000000..ead1ae12 --- /dev/null +++ b/lib/puppet-lint/plugins/check_strings.rb @@ -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("" => :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 index 00000000..8519e77c --- /dev/null +++ b/lib/puppet-lint/plugins/check_variables.rb @@ -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 index 00000000..6de0233a --- /dev/null +++ b/lib/puppet-lint/plugins/check_whitespace.rb @@ -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 index 00000000..ffd7a659 --- /dev/null +++ b/lib/puppet-lint/tasks/puppet-lint.rb @@ -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