require 'pp'
require 'yaml'
require 'simplabs/excellent/parsing/parser'
require 'simplabs/excellent/parsing/code_processor'
module Simplabs
module Excellent
# The Runner is the interface to invoke parsing and processing of source code. You can pass either a String containing the code to process or the
# name of a file to read the code to process from.
class Runner
DEFAULT_CHECKS_CONFIG = {
:AbcMetricMethodCheck => {},
:AssignmentInConditionalCheck => {},
:CaseMissingElseCheck => {},
:ClassLineCountCheck => {},
:ClassNameCheck => {},
:ControlCouplingCheck => {},
:CyclomaticComplexityBlockCheck => {},
:CyclomaticComplexityMethodCheck => {},
:EmptyRescueBodyCheck => {},
:FlogBlockCheck => {},
:FlogClassCheck => {},
:FlogMethodCheck => {},
:ForLoopCheck => {},
:GlobalVariableCheck => {},
:MethodLineCountCheck => {},
:MethodNameCheck => {},
:ModuleLineCountCheck => {},
:ModuleNameCheck => {},
:ParameterNumberCheck => {},
:ClassVariableCheck => {},
:'Rails::AttrProtectedCheck' => {},
:'Rails::AttrAccessibleCheck' => {},
:'Rails::InstanceVarInPartialCheck' => {},
:'Rails::ValidationsCheck' => {},
:'Rails::ParamsHashInViewCheck' => {},
:'Rails::SessionHashInViewCheck' => {},
:'Rails::CustomInitializeMethodCheck' => {}
}
attr_reader :checks
# Initializes a Runner
#
# ==== Parameters
#
# * checks_config - The check configuration to use; You can either specify an array of specs to use like this
# [:ClassLineCountCheck => { :threshold => 10 }]
# or you can specify a hash that will then be merged with the default configuration:
# { :ClassNameCheck => { pattern: 'test' }, :ClassLineCountCheck => { :threshold => 10 } }
# You can enable/disable a check by setting the value of the hash to sth. truthy/falsy:
# { :ClassNameCheck => false, :AbcMetricMethodCheck => {} }
def initialize(checks_config = {})
@checks = load_checks(checks_config)
@parser = Parsing::Parser.new
end
# Processes the +code+ and sets the file name of the warning to +filename+
#
# ==== Parameters
#
# * filename - The name of the file the code was read from.
# * code - The code to process (String).
def check(filename, code)
@processor ||= Parsing::CodeProcessor.new(@checks)
node = parse(filename, code)
@processor.process(node)
end
# Processes the +code+, setting the file name of the warnings to '+dummy-file.rb+'
#
# ==== Parameters
#
# * code - The code to process (String).
def check_code(code)
check('dummy-file.rb', code)
end
# Processes the file +filename+. The code will be read from the file.
#
# ==== Parameters
#
# * filename - The name of the file to read the code from.
def check_file(filename)
check(filename, File.read(filename))
end
# Processes the passed +paths+
#
# ==== Parameters
#
# * paths - The paths to process (specify file names or directories; will recursively process all ruby files if a directory is given).
# * formatter - The formatter to use. If a formatter is specified, its +start+, +file+, +warning+ and +end+ methods will be called
def check_paths(paths, formatter = nil)
formatter.start if formatter
collect_files(paths).each do |path|
check_file(path)
format_file_and_warnings(formatter, path) if formatter
end
formatter.end if formatter
end
# Gets the warnings that were produced by the checks.
def warnings
@checks.collect { |check| check.warnings }.flatten
end
private
def parse(filename, code)
@parser.parse(code, filename)
end
def load_checks(checks_config)
effective_checks = checks_config.is_a?(Array) ? checks_config : [DEFAULT_CHECKS_CONFIG.deep_merge(checks_config.deep_symbolize_keys)]
check_objects = []
effective_checks.each do |check|
check.each do |name, check_config|
if !!check_config
klass = name.to_s.split('::').inject(::Simplabs::Excellent::Checks) do |mod, class_name|
mod.const_get(class_name)
end
check_objects << klass.new(check_config.is_a?(Hash) ? check_config.deep_symbolize_keys : {})
end
end
end
check_objects
end
def collect_files(paths)
files = []
paths.each do |path|
if File.file?(path)
files << path
elsif File.directory?(path)
files += Dir.glob(File.join(path, '**/*.{rb,erb}'))
else
raise ArgumentError.new("#{path} is neither a File nor a directory!")
end
end
files
end
def format_file_and_warnings(formatter, filename)
warnings = @checks.map { |check| check.warnings_for(filename) }.flatten
if warnings.length > 0
formatter.file(filename) do |formatter|
warnings.sort { |x, y| x.line_number <=> y.line_number }.each { |warning| formatter.warning(warning) }
end
end
end
end
end
end