reek 1.6.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +6 -9
  3. data/features/command_line_interface/options.feature +20 -16
  4. data/features/command_line_interface/stdin.feature +1 -1
  5. data/features/rake_task/rake_task.feature +0 -12
  6. data/features/reports/reports.feature +63 -23
  7. data/features/reports/yaml.feature +3 -3
  8. data/features/samples.feature +3 -3
  9. data/lib/reek/cli/application.rb +5 -5
  10. data/lib/reek/cli/input.rb +1 -1
  11. data/lib/reek/cli/option_interpreter.rb +77 -0
  12. data/lib/reek/cli/options.rb +89 -82
  13. data/lib/reek/cli/report/formatter.rb +33 -24
  14. data/lib/reek/cli/report/heading_formatter.rb +45 -0
  15. data/lib/reek/cli/report/location_formatter.rb +23 -0
  16. data/lib/reek/cli/report/report.rb +32 -17
  17. data/lib/reek/configuration/app_configuration.rb +2 -2
  18. data/lib/reek/configuration/configuration_file_finder.rb +10 -10
  19. data/lib/reek/core/smell_repository.rb +3 -28
  20. data/lib/reek/rake/task.rb +35 -76
  21. data/lib/reek/smell_warning.rb +31 -16
  22. data/lib/reek/smells/nested_iterators.rb +1 -1
  23. data/lib/reek/smells/smell_detector.rb +9 -0
  24. data/lib/reek/smells/utility_function.rb +2 -1
  25. data/lib/reek/spec/should_reek.rb +0 -3
  26. data/lib/reek/spec/should_reek_of.rb +61 -12
  27. data/lib/reek/spec/should_reek_only_of.rb +12 -10
  28. data/lib/reek/version.rb +1 -1
  29. data/reek.gemspec +2 -2
  30. data/spec/factories/factories.rb +2 -5
  31. data/spec/reek/cli/html_report_spec.rb +28 -0
  32. data/spec/reek/cli/option_interperter_spec.rb +14 -0
  33. data/spec/reek/cli/text_report_spec.rb +95 -0
  34. data/spec/reek/cli/yaml_report_spec.rb +23 -0
  35. data/spec/reek/configuration/configuration_file_finder_spec.rb +5 -6
  36. data/spec/reek/core/module_context_spec.rb +1 -1
  37. data/spec/reek/core/smell_repository_spec.rb +17 -0
  38. data/spec/reek/smell_warning_spec.rb +9 -11
  39. data/spec/reek/smells/boolean_parameter_spec.rb +11 -11
  40. data/spec/reek/smells/control_parameter_spec.rb +40 -40
  41. data/spec/reek/smells/data_clump_spec.rb +17 -17
  42. data/spec/reek/smells/duplicate_method_call_spec.rb +56 -33
  43. data/spec/reek/smells/feature_envy_spec.rb +44 -40
  44. data/spec/reek/smells/irresponsible_module_spec.rb +1 -1
  45. data/spec/reek/smells/long_parameter_list_spec.rb +12 -12
  46. data/spec/reek/smells/long_yield_list_spec.rb +4 -4
  47. data/spec/reek/smells/module_initialize_spec.rb +3 -3
  48. data/spec/reek/smells/nested_iterators_spec.rb +71 -52
  49. data/spec/reek/smells/nil_check_spec.rb +6 -6
  50. data/spec/reek/smells/prima_donna_method_spec.rb +2 -2
  51. data/spec/reek/smells/too_many_statements_spec.rb +34 -34
  52. data/spec/reek/smells/uncommunicative_method_name_spec.rb +1 -1
  53. data/spec/reek/smells/uncommunicative_module_name_spec.rb +7 -3
  54. data/spec/reek/smells/uncommunicative_parameter_name_spec.rb +12 -12
  55. data/spec/reek/smells/uncommunicative_variable_name_spec.rb +28 -38
  56. data/spec/reek/smells/unused_parameters_spec.rb +16 -17
  57. data/spec/reek/smells/utility_function_spec.rb +21 -8
  58. data/spec/reek/spec/should_reek_of_spec.rb +18 -5
  59. data/spec/reek/spec/should_reek_only_of_spec.rb +7 -1
  60. data/spec/spec_helper.rb +22 -14
  61. metadata +15 -20
  62. data/lib/reek/cli/help_command.rb +0 -15
  63. data/lib/reek/cli/report/strategy.rb +0 -64
  64. data/lib/reek/cli/version_command.rb +0 -16
  65. data/spec/matchers/smell_of_matcher.rb +0 -95
  66. data/spec/reek/cli/help_command_spec.rb +0 -25
  67. data/spec/reek/cli/report_spec.rb +0 -132
  68. data/spec/reek/cli/version_command_spec.rb +0 -31
@@ -1,8 +1,14 @@
1
+ require 'reek/cli/report/location_formatter'
2
+
1
3
  module Reek
2
4
  module Cli
3
5
  module Report
6
+ #
7
+ # Formatter handling the formatting of the report at large. Formatting
8
+ # the individual warnings is handled by the warning formatter passed in.
9
+ #
4
10
  module Formatter
5
- def self.format_list(warnings, formatter = SimpleWarningFormatter)
11
+ def self.format_list(warnings, formatter = SimpleWarningFormatter.new)
6
12
  warnings.map do |warning|
7
13
  " #{formatter.format warning}"
8
14
  end.join("\n")
@@ -17,41 +23,44 @@ module Reek
17
23
  end
18
24
  end
19
25
 
20
- module UltraVerboseWarningFormattter
21
- BASE_URL_FOR_HELP_LINK = 'https://github.com/troessner/reek/wiki/'
22
-
23
- module_function
26
+ #
27
+ # Basic formatter that just shows a simple message for each warning,
28
+ # pre-pended with the result of the passed-in location formatter.
29
+ #
30
+ class SimpleWarningFormatter
31
+ def initialize(location_formatter = BlankLocationFormatter)
32
+ @location_formatter = location_formatter
33
+ end
24
34
 
25
35
  def format(warning)
26
- "#{WarningFormatterWithLineNumbers.format(warning)} " \
27
- "[#{explanatory_link(warning)}]"
36
+ "#{@location_formatter.format(warning)}#{base_format(warning)}"
28
37
  end
29
38
 
30
- def explanatory_link(warning)
31
- "#{BASE_URL_FOR_HELP_LINK}#{class_name_to_param(warning.smell_type)}"
32
- end
39
+ private
33
40
 
34
- def class_name_to_param(name)
35
- name.split(/(?=[A-Z])/).join('-')
41
+ def base_format(warning)
42
+ "#{warning.context} #{warning.message} (#{warning.smell_type})"
36
43
  end
37
44
  end
38
45
 
39
- module SimpleWarningFormatter
40
- def self.format(warning)
41
- "#{warning.context} #{warning.message} (#{warning.smell_type})"
46
+ #
47
+ # Formatter that adds a link to the Wiki to the basic message from
48
+ # SimpleWarningFormatter.
49
+ #
50
+ class WikiLinkWarningFormatter < SimpleWarningFormatter
51
+ BASE_URL_FOR_HELP_LINK = 'https://github.com/troessner/reek/wiki/'
52
+
53
+ def format(warning)
54
+ "#{super} " \
55
+ "[#{explanatory_link(warning)}]"
42
56
  end
43
- end
44
57
 
45
- module WarningFormatterWithLineNumbers
46
- def self.format(warning)
47
- "#{warning.lines.inspect}:#{SimpleWarningFormatter.format(warning)}"
58
+ def explanatory_link(warning)
59
+ "#{BASE_URL_FOR_HELP_LINK}#{class_name_to_param(warning.smell_type)}"
48
60
  end
49
- end
50
61
 
51
- module SingleLineWarningFormatter
52
- def self.format(warning)
53
- "#{warning.source}:#{warning.lines.first}: " \
54
- "#{SimpleWarningFormatter.format(warning)}"
62
+ def class_name_to_param(name)
63
+ name.split(/(?=[A-Z])/).join('-')
55
64
  end
56
65
  end
57
66
  end
@@ -0,0 +1,45 @@
1
+ module Reek
2
+ module Cli
3
+ module Report
4
+ module HeadingFormatter
5
+ #
6
+ # Base class for heading formatters.
7
+ # Is responsible for formatting the heading emitted for each examiner
8
+ #
9
+ class Base
10
+ attr_reader :report_formatter
11
+
12
+ def initialize(report_formatter)
13
+ @report_formatter = report_formatter
14
+ end
15
+
16
+ def header(examiner)
17
+ if show_header?(examiner)
18
+ report_formatter.header examiner
19
+ else
20
+ ''
21
+ end
22
+ end
23
+ end
24
+
25
+ #
26
+ # Lists out each examiner, even if it has no smell
27
+ #
28
+ class Verbose < Base
29
+ def show_header?(_examiner)
30
+ true
31
+ end
32
+ end
33
+
34
+ #
35
+ # Lists only smelly examiners
36
+ #
37
+ class Quiet < Base
38
+ def show_header?(examiner)
39
+ examiner.smelly?
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ module Reek
2
+ module Cli
3
+ module Report
4
+ module BlankLocationFormatter
5
+ def self.format(_warning)
6
+ ''
7
+ end
8
+ end
9
+
10
+ module DefaultLocationFormatter
11
+ def self.format(warning)
12
+ "#{warning.lines.inspect}:"
13
+ end
14
+ end
15
+
16
+ module SingleLineLocationFormatter
17
+ def self.format(warning)
18
+ "#{warning.source}:#{warning.lines.first}: "
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -11,13 +11,9 @@ module Reek
11
11
  NO_WARNINGS_COLOR = :green
12
12
  WARNINGS_COLOR = :red
13
13
 
14
- def initialize(options = {})
15
- @warning_formatter = options.fetch :warning_formatter, SimpleWarningFormatter
16
- @report_formatter = options.fetch :report_formatter, Formatter
14
+ def initialize(_options = {})
17
15
  @examiners = []
18
16
  @total_smell_count = 0
19
- @sort_by_issue_count = options.fetch :sort_by_issue_count, false
20
- @strategy = options.fetch(:strategy, Strategy::Quiet)
21
17
  end
22
18
 
23
19
  def add_examiner(examiner)
@@ -31,7 +27,7 @@ module Reek
31
27
  end
32
28
 
33
29
  def smells
34
- @strategy.new(@report_formatter, @warning_formatter, @examiners).gather_results
30
+ @examiners.map(&:smells).flatten
35
31
  end
36
32
  end
37
33
 
@@ -39,12 +35,26 @@ module Reek
39
35
  # Generates a sorted, text summary of smells in examiners
40
36
  #
41
37
  class TextReport < Base
38
+ def initialize(options = {})
39
+ super options
40
+ @options = options
41
+ @warning_formatter = options.fetch :warning_formatter, SimpleWarningFormatter.new
42
+ @report_formatter = options.fetch :report_formatter, Formatter
43
+ @sort_by_issue_count = options.fetch :sort_by_issue_count, false
44
+ end
45
+
42
46
  def show
43
47
  sort_examiners if smells?
44
48
  display_summary
45
49
  display_total_smell_count
46
50
  end
47
51
 
52
+ def smells
53
+ @examiners.each_with_object([]) do |examiner, result|
54
+ result << summarize_single_examiner(examiner)
55
+ end
56
+ end
57
+
48
58
  private
49
59
 
50
60
  def display_summary
@@ -57,6 +67,16 @@ module Reek
57
67
  print total_smell_count_message
58
68
  end
59
69
 
70
+ def summarize_single_examiner(examiner)
71
+ result = heading_formatter.header(examiner)
72
+ if examiner.smelly?
73
+ formatted_list = @report_formatter.format_list(examiner.smells,
74
+ @warning_formatter)
75
+ result += ":\n#{formatted_list}"
76
+ end
77
+ result
78
+ end
79
+
60
80
  def sort_examiners
61
81
  @examiners.sort_by!(&:smells_count).reverse! if @sort_by_issue_count
62
82
  end
@@ -66,19 +86,19 @@ module Reek
66
86
  s = @total_smell_count == 1 ? '' : 's'
67
87
  Rainbow("#{@total_smell_count} total warning#{s}\n").color(colour)
68
88
  end
89
+
90
+ def heading_formatter
91
+ @heading_formatter ||=
92
+ @options.fetch(:heading_formatter, HeadingFormatter::Quiet).new(@report_formatter)
93
+ end
69
94
  end
70
95
 
71
96
  #
72
97
  # Displays a list of smells in YAML format
73
98
  # YAML with empty array for 0 smells
74
99
  class YamlReport < Base
75
- def initialize(options = {})
76
- @options = options
77
- super options.merge!(strategy: Strategy::Normal)
78
- end
79
-
80
100
  def show
81
- print(smells.to_yaml)
101
+ print smells.map(&:yaml_hash).to_yaml
82
102
  end
83
103
  end
84
104
 
@@ -86,11 +106,6 @@ module Reek
86
106
  # Saves the report as a HTML file
87
107
  #
88
108
  class HtmlReport < Base
89
- def initialize(options = {})
90
- @options = options
91
- super @options.merge!(strategy: Strategy::Normal)
92
- end
93
-
94
109
  require 'erb'
95
110
 
96
111
  def show
@@ -13,9 +13,9 @@ module Reek
13
13
  class << self
14
14
  attr_reader :configuration
15
15
 
16
- def initialize_with(application)
16
+ def initialize_with(options)
17
17
  @has_been_initialized = true
18
- configuration_file_path = ConfigurationFileFinder.find(application)
18
+ configuration_file_path = ConfigurationFileFinder.find(options)
19
19
  return unless configuration_file_path
20
20
  load_from_file configuration_file_path
21
21
  end
@@ -6,26 +6,26 @@ module Reek
6
6
  # ConfigurationFileFinder is responsible for finding reeks configuration.
7
7
  #
8
8
  # There are 3 ways of passing `reek` a configuration file:
9
- # 1. Using the cli "-c" switch (see "Command line interface" above)
10
- # 2. Having a file ending with .reek either in your current working directory or in a parent
11
- # directory
9
+ # 1. Using the cli "-c" switch
10
+ # 2. Having a file ending with .reek either in your current working
11
+ # directory or in a parent directory
12
12
  # 3. Having a file ending with .reek in your HOME directory
13
13
  #
14
- # The order in which ConfigurationFileFinder tries to find such a configuration file is exactly
15
- # like above.
14
+ # The order in which ConfigurationFileFinder tries to find such a
15
+ # configuration file is exactly like above.
16
16
  module ConfigurationFileFinder
17
17
  class << self
18
- def find(application)
19
- configuration_by_cli(application) ||
18
+ def find(options)
19
+ configuration_by_cli(options) ||
20
20
  configuration_in_file_system ||
21
21
  configuration_in_home_directory
22
22
  end
23
23
 
24
24
  private
25
25
 
26
- def configuration_by_cli(application)
27
- return unless application # Return gracefully allowing calls without app context
28
- config_file_option = application.options.config_file
26
+ def configuration_by_cli(options)
27
+ return unless options # Return gracefully allowing calls without app context
28
+ config_file_option = options.config_file
29
29
  return unless config_file_option
30
30
  path_name = Pathname.new config_file_option
31
31
  raise ArgumentError, "Config file #{path_name} doesn't exist" unless path_name.exist?
@@ -1,4 +1,5 @@
1
1
  require 'reek/smells'
2
+ require 'reek/smells/smell_detector'
2
3
 
3
4
  module Reek
4
5
  module Core
@@ -9,36 +10,10 @@ module Reek
9
10
  attr_reader :detectors
10
11
 
11
12
  def self.smell_types
12
- # SMELL: Duplication -- these should be loaded by listing the files
13
- [
14
- Smells::Attribute,
15
- Smells::BooleanParameter,
16
- Smells::ClassVariable,
17
- Smells::ControlParameter,
18
- Smells::DataClump,
19
- Smells::DuplicateMethodCall,
20
- Smells::FeatureEnvy,
21
- Smells::IrresponsibleModule,
22
- Smells::LongParameterList,
23
- Smells::LongYieldList,
24
- Smells::ModuleInitialize,
25
- Smells::NestedIterators,
26
- Smells::NilCheck,
27
- Smells::PrimaDonnaMethod,
28
- Smells::RepeatedConditional,
29
- Smells::TooManyInstanceVariables,
30
- Smells::TooManyMethods,
31
- Smells::TooManyStatements,
32
- Smells::UncommunicativeMethodName,
33
- Smells::UncommunicativeModuleName,
34
- Smells::UncommunicativeParameterName,
35
- Smells::UncommunicativeVariableName,
36
- Smells::UnusedParameters,
37
- Smells::UtilityFunction
38
- ]
13
+ Reek::Smells::SmellDetector.descendants
39
14
  end
40
15
 
41
- def initialize(source_description, smell_types = SmellRepository.smell_types)
16
+ def initialize(source_description, smell_types = self.class.smell_types)
42
17
  @typed_detectors = nil
43
18
  @detectors = {}
44
19
  smell_types.each do |klass|
@@ -3,6 +3,7 @@
3
3
  require 'rake'
4
4
  require 'rake/tasklib'
5
5
  require 'pathname'
6
+ require 'English'
6
7
 
7
8
  module Reek
8
9
  #
@@ -32,126 +33,84 @@ module Reek
32
33
  # rake reek REEK_OPTS=-s # sorts the report by smell
33
34
  #
34
35
  class Task < ::Rake::TaskLib
35
- # Name of reek task.
36
- # Defaults to :reek.
37
- attr_accessor :name
36
+ # Name of reek task. Defaults to :reek.
37
+ attr_writer :name
38
38
 
39
- # Array of directories to be added to $LOAD_PATH before running reek.
40
- # Defaults to ['<the absolute path to reek's lib directory>']
41
- attr_accessor :libs
42
-
43
- # Glob pattern to match config files.
39
+ # Path to reeks config file.
44
40
  # Setting the REEK_CFG environment variable overrides this.
45
- # Defaults to 'config/**/*.reek'.
46
- attr_accessor :config_file
47
-
48
- def config_files=(value)
49
- @config_file = value
50
- end
51
-
52
- def config_files
53
- @config_file
54
- end
41
+ attr_writer :config_file
55
42
 
56
43
  # Glob pattern to match source files.
57
44
  # Setting the REEK_SRC environment variable overrides this.
58
45
  # Defaults to 'lib/**/*.rb'.
59
- attr_accessor :source_files
46
+ attr_writer :source_files
60
47
 
61
48
  # String containing commandline options to be passed to Reek.
62
49
  # Setting the REEK_OPTS environment variable overrides this value.
63
50
  # Defaults to ''.
64
- attr_accessor :reek_opts
65
-
66
- # Array of commandline options to pass to ruby. Defaults to [].
67
- attr_accessor :ruby_opts
51
+ attr_writer :reek_opts
68
52
 
69
53
  # Whether or not to fail Rake when an error occurs (typically when smells are found).
70
54
  # Defaults to true.
71
- attr_accessor :fail_on_error
55
+ attr_writer :fail_on_error
72
56
 
73
57
  # Use verbose output. If this is set to true, the task will print
74
58
  # the reek command to stdout. Defaults to false.
75
- attr_accessor :verbose
59
+ attr_writer :verbose
76
60
 
77
- # Defines a new task, using the name +name+.
78
61
  def initialize(name = :reek)
79
- @name = name
80
- @libs = [File.expand_path(File.dirname(__FILE__) + '/../../../lib')]
81
- @config_file = nil
82
- @source_files = nil
83
- @ruby_opts = []
84
- @reek_opts = ''
62
+ @name = name
63
+ @reek_opts = ''
85
64
  @fail_on_error = true
86
- @sort = nil
65
+ @source_files = 'lib/**/*.rb'
66
+ @verbose = false
87
67
 
88
68
  yield self if block_given?
89
- @config_file ||= Pathname.glob('config/**/*.reek').detect(&:file?)
90
- @source_files ||= 'lib/**/*.rb'
91
- define
69
+ define_task
92
70
  end
93
71
 
94
72
  private
95
73
 
96
- def define # :nodoc:
97
- desc 'Check for code smells' unless ::Rake.application.last_comment
98
- task(name) { run_task }
99
- self
74
+ def define_task
75
+ desc 'Check for code smells'
76
+ task(@name) { run_task }
100
77
  end
101
78
 
102
79
  def run_task
103
- return if source_file_list.empty?
104
- cmd = cmd_words.join(' ')
105
- puts cmd if @verbose
106
- raise('Smells found!') if !system(cmd) && fail_on_error
80
+ puts "\n\n!!! Running 'reek' rake command: #{command}\n\n" if @verbose
81
+ system(*command)
82
+ abort("\n\n!!! `reek` has found smells - exiting!") if sys_call_failed? && @fail_on_error
107
83
  end
108
84
 
109
- def self.ruby_exe
110
- File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
85
+ def command
86
+ ['reek', *config_file_as_argument, *reek_opts_as_arguments, *source_files].
87
+ compact.
88
+ reject(&:empty?)
111
89
  end
112
90
 
113
- def cmd_words
114
- [Task.ruby_exe] +
115
- ruby_options +
116
- [%(reek)] +
117
- [sort_option] +
118
- config_option +
119
- source_file_list.map { |fn| %("#{fn}") }
91
+ def source_files
92
+ FileList[ENV['REEK_SRC'] || @source_files]
120
93
  end
121
94
 
122
- def config_option
123
- if config_file
124
- ['-c', config_file]
125
- else
126
- []
127
- end
95
+ def reek_opts
96
+ ENV['REEK_OPTS'] || @reek_opts
128
97
  end
129
98
 
130
99
  def config_file
131
100
  ENV['REEK_CFG'] || @config_file
132
101
  end
133
102
 
134
- def ruby_options
135
- if bundler?
136
- %w(-S bundle exec)
137
- else
138
- lib_path = @libs.join(File::PATH_SEPARATOR)
139
- @ruby_opts.clone << "-I\"#{lib_path}\""
140
- end
103
+ def sys_call_failed?
104
+ !$CHILD_STATUS.success?
141
105
  end
142
106
 
143
- def bundler?
144
- File.exist?('./Gemfile')
145
- end
146
-
147
- def sort_option
148
- ENV['REEK_OPTS'] || @reek_opts
107
+ def config_file_as_argument
108
+ return [] unless @config_file
109
+ ['-c', @config_file]
149
110
  end
150
111
 
151
- def source_file_list # :nodoc:
152
- files = ENV['REEK_SRC'] || @source_files
153
- return [] unless files
154
- FileList[files]
112
+ def reek_opts_as_arguments
113
+ reek_opts.split(/\s+/)
155
114
  end
156
115
  end
157
116
  end