lib/reek/configuration/app_configuration.rb in reek-3.0.4 vs lib/reek/configuration/app_configuration.rb in reek-3.1

- old
+ new

@@ -1,90 +1,144 @@ +require 'pathname' require_relative './configuration_file_finder' module Reek # @api private module Configuration # @api private - class ConfigFileException < StandardError; end # # Reek's singleton configuration instance. # # @api private - module AppConfiguration - NON_SMELL_TYPE_KEYS = %w(exclude_paths) - EXCLUDE_PATHS_KEY = 'exclude_paths' - @configuration = {} - @has_been_initialized = false + class AppConfiguration + EXCLUDE_PATHS_KEY = 'exclude_paths' + attr_reader :exclude_paths, :default_directive, :directory_directives - class << self - attr_reader :configuration + # Given this configuration file: + # + # --- + # IrresponsibleModule: + # enabled: false + # "app/helpers": + # UtilityFunction: + # enabled: false + # exclude_paths: + # - "app/controllers" + # + # this would result in the following configuration: + # + # exclude_paths = [ Pathname('spec/samples/two_smelly_files') ] + # default_directive = { Reek::Smells::IrresponsibleModule => { "enabled" => false } } + # directory_directives = { Pathname("spec/samples/three_clean_files/") => + # { Reek::Smells::UtilityFunction => { "enabled" => false } } } + # + # + # @param options [OpenStruct] + # e.g.: #<OpenStruct report_format =: text, + # location_format =: numbers, + # colored = true, + # smells_to_detect = [], + # config_file = #<Pathname:config/defaults.reek>, + # argv = [ "lib/reek/spec" ]> + def initialize(options = nil) + self.directory_directives = {} + self.default_directive = {} + self.exclude_paths = [] - def initialize_with(options) - @has_been_initialized = true - configuration_file_path = ConfigurationFileFinder.find(options: options) - return unless configuration_file_path - load_from_file configuration_file_path - end + load options + end - def configure_smell_repository(smell_repository) - # Let users call this method directly without having initialized AppConfiguration before - # and if they do, initialize it without application context - initialize_with(nil) unless @has_been_initialized - for_smell_types.each do |klass_name, config| - klass = load_smell_type(klass_name) - smell_repository.configure(klass, config) if klass - end - end + # @param source_via [String] - the source of the code inspected + # @return [Hash] the directory directive for the source or, if there is + # none, the default directive + def directive_for(source_via) + directory_directive_for_source(source_via) || default_directive + end - def load_from_file(path) - if File.size(path) == 0 - report_problem('Empty file', path) - return - end + private - begin - @configuration = YAML.load_file(path) || {} - rescue => error - raise_error(error.to_s, path) - end + attr_writer :exclude_paths, :default_directive, :directory_directives - raise_error('Not a hash', path) unless @configuration.is_a? Hash - end + # @param source_via [String] - the source of the code inspected + # Might be a string, STDIN or Filename / Pathname. We're only interested in the source + # when it's coming from file since a directory_directive doesn't make sense + # for anything else. + # @return [Hash | nil] the configuration for the source or nil + def directory_directive_for_source(source_via) + return unless source_via + source_base_dir = Pathname.new(source_via).dirname + hit = best_directory_match_for source_base_dir + directory_directives[hit] + end - def reset - @configuration.clear + def load(options) + configuration_file = ConfigurationFileFinder.find_and_load(options: options) + + configuration_file.each do |key, value| + case + when key == EXCLUDE_PATHS_KEY + handle_exclude_paths(value) + when smell_type?(key) + handle_default_directive(key, value) + else + handle_directory_directive(key, value) + end end + end - def exclude_paths - @exclude_paths ||= @configuration. - fetch(EXCLUDE_PATHS_KEY, []). - map { |path| path.chomp('/') } + def handle_exclude_paths(paths) + self.exclude_paths = paths.map do |path| + pathname = Pathname.new path.chomp('/') + raise ArgumentError, "Excluded directory #{path} does not exists" unless pathname.exist? + pathname end + end - private + def handle_default_directive(key, config) + klass = Reek::Smells.const_get(key) + default_directive[klass] = config + end - def for_smell_types - @configuration.reject { |key, _value| NON_SMELL_TYPE_KEYS.include?(key) } - end + def handle_directory_directive(path, config) + pathname = Pathname.new path.chomp('/') + validate_directive pathname - def load_smell_type(name) - Reek::Smells.const_get(name) - rescue NameError - report_problem("\"#{name}\" is not a code smell") - nil + directory_directives[pathname] = config.each_with_object({}) do |(key, value), hash| + abort(error_message_for_invalid_smell_type(key)) unless smell_type?(key) + hash[Reek::Smells.const_get(key)] = value end + end - def report_problem(reason, path) - $stderr.puts "Warning: #{message(reason, path)}" - end + def best_directory_match_for(source_base_dir) + directory_directives. + keys. + select { |pathname| source_base_dir.to_s =~ /#{pathname}/ }. + max_by { |pathname| pathname.to_s.length } + end - def raise_error(reason, path) - raise ConfigFileException, message(reason, path) - end + def smell_type?(key) + Reek::Smells.const_get key + rescue NameError + false + end - def message(reason, path) - "Invalid configuration file \"#{File.basename(path)}\" -- #{reason}" - end + def error_message_for_invalid_smell_type(klass) + "You are trying to configure smell type #{klass} but we can't find one with that name.\n" \ + "Please make sure you spelled it right (see 'config/defaults.reek' in the reek\n" \ + 'repository for a list of all available smell types.' + end + + def error_message_for_missing_directory(pathname) + "Configuration error: Directory `#{pathname}` does not exist" + end + + def error_message_for_file_given(pathname) + "Configuration error: `#{pathname}` is supposed to be a directory but is a file" + end + + def validate_directive(pathname) + abort(error_message_for_missing_directory(pathname)) unless pathname.exist? + abort(error_message_for_file_given(pathname)) unless pathname.directory? end end end end