reek 1.6.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -34,36 +34,51 @@ module Reek
34
34
  (self <=> other) == 0
35
35
  end
36
36
 
37
- def matches?(klass, patterns)
38
- smell_classes.include?(klass.to_s) && contains_all?(patterns)
37
+ def matches?(klass, other_parameters = {})
38
+ smell_classes.include?(klass.to_s) && common_parameters_equal?(other_parameters)
39
39
  end
40
40
 
41
41
  def report_on(listener)
42
42
  listener.found_smell(self)
43
43
  end
44
44
 
45
- def encode_with(coder)
46
- coder.tag = nil
47
- coder['smell_category'] = smell_detector.smell_category
48
- coder['smell_type'] = smell_detector.smell_type
49
- coder['source'] = smell_detector.source
50
- coder['context'] = context
51
- coder['lines'] = lines
52
- coder['message'] = message
45
+ def yaml_hash
46
+ result = {
47
+ 'smell_category' => smell_detector.smell_category,
48
+ 'smell_type' => smell_detector.smell_type,
49
+ 'source' => smell_detector.source,
50
+ 'context' => context,
51
+ 'lines' => lines,
52
+ 'message' => message
53
+ }
53
54
  parameters.each do |key, value|
54
- coder[key.to_s] = value
55
+ result[key.to_s] = value
55
56
  end
57
+ result
56
58
  end
57
59
 
58
60
  protected
59
61
 
60
- def contains_all?(patterns)
61
- rpt = sort_key.to_s
62
- patterns.all? { |pattern| pattern =~ rpt }
63
- end
64
-
65
62
  def sort_key
66
63
  [context, message, smell_category]
67
64
  end
65
+
66
+ def common_parameters_equal?(other_parameters)
67
+ other_parameters.keys.each do |key|
68
+ unless parameters.key?(key)
69
+ raise ArgumentError, "The parameter #{key} you want to check for doesn't exist"
70
+ end
71
+ end
72
+
73
+ # Why not check for strict parameter equality instead of just the common ones?
74
+ #
75
+ # In `self`, `parameters` might look like this: {:name=>"@other.thing", :count=>2}
76
+ # Coming from specs, 'other_parameters' might look like this, e.g.:
77
+ # {:name=>"@other.thing"}
78
+ # So in this spec we are just specifying the "name" parameter but not the "count".
79
+ # In order to allow for this kind of leniency we just test for common parameter equality,
80
+ # not for a strict one.
81
+ parameters.values_at(*other_parameters.keys) == other_parameters.values
82
+ end
68
83
  end
69
84
  end
@@ -39,7 +39,7 @@ module Reek
39
39
  context: ctx.full_name,
40
40
  lines: [exp.line],
41
41
  message: "contains iterators nested #{depth} deep",
42
- parameters: { count: depth })]
42
+ parameters: { name: ctx.full_name, count: depth })]
43
43
  else
44
44
  []
45
45
  end
@@ -36,6 +36,15 @@ module Reek
36
36
  EXCLUDE_KEY => DEFAULT_EXCLUDE_SET.dup
37
37
  }
38
38
  end
39
+
40
+ def inherited(subclass)
41
+ @subclasses ||= []
42
+ @subclasses << subclass
43
+ end
44
+
45
+ def descendants
46
+ @subclasses
47
+ end
39
48
  end
40
49
 
41
50
  def smell_category
@@ -69,7 +69,8 @@ module Reek
69
69
  [SmellWarning.new(self,
70
70
  context: method_ctx.full_name,
71
71
  lines: [method_ctx.exp.line],
72
- message: "doesn't depend on instance state")]
72
+ message: "doesn't depend on instance state",
73
+ parameters: { name: method_ctx.full_name })]
73
74
  end
74
75
 
75
76
  private
@@ -1,7 +1,5 @@
1
1
  require 'reek/examiner'
2
- require 'reek/cli/report/report'
3
2
  require 'reek/cli/report/formatter'
4
- require 'reek/cli/report/strategy'
5
3
 
6
4
  module Reek
7
5
  module Spec
@@ -23,7 +21,6 @@ module Reek
23
21
  "Expected no smells, but got:\n#{rpt}"
24
22
  end
25
23
  end
26
-
27
24
  #
28
25
  # Returns +true+ if and only if the target source code contains smells.
29
26
  #
@@ -6,34 +6,83 @@ module Reek
6
6
  # An rspec matcher that matches when the +actual+ has the specified
7
7
  # code smell.
8
8
  #
9
- class ShouldReekOf # :nodoc:
10
- def initialize(klass, patterns)
11
- @klass = klass
12
- @patterns = patterns
9
+ class ShouldReekOf
10
+ def initialize(smell_category, smell_details = {})
11
+ @smell_category = normalize smell_category
12
+ @smell_details = smell_details
13
13
  end
14
14
 
15
15
  def matches?(actual)
16
16
  @examiner = Examiner.new(actual)
17
17
  @all_smells = @examiner.smells
18
- @all_smells.any? { |warning| warning.matches?(@klass, @patterns) }
18
+ @all_smells.any? { |warning| warning.matches?(@smell_category, @smell_details) }
19
19
  end
20
20
 
21
21
  def failure_message
22
- "Expected #{@examiner.description} to reek of #{@klass}, but it didn't"
22
+ "Expected #{@examiner.description} to reek of #{@smell_category}, but it didn't"
23
23
  end
24
24
 
25
25
  def failure_message_when_negated
26
- "Expected #{@examiner.description} not to reek of #{@klass}, but it did"
26
+ "Expected #{@examiner.description} not to reek of #{@smell_category}, but it did"
27
+ end
28
+
29
+ private
30
+
31
+ def normalize(smell_category_or_type)
32
+ # In theory, users can give us many different types of input (see the documentation for
33
+ # reek_of below), however we're basically ignoring all of those subleties and just
34
+ # return a string with the prepending namespace stripped.
35
+ smell_category_or_type.to_s.split(/::/)[-1]
27
36
  end
28
37
  end
29
38
 
30
39
  #
31
- # Checks the target source code for instances of +smell_category+,
32
- # and returns +true+ only if one of them has a report string matching
33
- # all of the +patterns+.
40
+ # Checks the target source code for instances of "smell category"
41
+ # and returns true only if it can find one of them that matches.
42
+ #
43
+ # Remember that this includes our "smell types" as well. So it could be the
44
+ # "smell type" UtilityFunction, which is represented as a concrete class
45
+ # in reek but it could also be "Duplication" which is a "smell categgory".
46
+ #
47
+ # In theory you could pass many different types of input here:
48
+ # - :UtilityFunction
49
+ # - "UtilityFunction"
50
+ # - UtilityFunction (this works in our specs because we tend to do "include Reek:Smells")
51
+ # - Reek::Smells::UtilityFunction (the right way if you really want to pass a class)
52
+ # - "Duplication" or :Duplication which is an abstract "smell category"
53
+ #
54
+ # It is recommended to pass this as a symbol like :UtilityFunction. However we don't
55
+ # enforce this.
56
+ #
57
+ # Additionally you can be more specific and pass in "smell_details" you
58
+ # want to check for as well e.g. "name" or "count" (see the examples below).
59
+ # The parameters you can check for are depending on the smell you are checking for.
60
+ # For instance "count" doesn't make sense everywhere whereas "name" does in most cases.
61
+ # If you pass in a parameter that doesn't exist (e.g. you make a typo like "namme") reek will
62
+ # raise an ArgumentError to give you a hint that you passed something that doesn't make
63
+ # much sense.
64
+ #
65
+ # smell_category - The "smell category" or "smell_type" we check for.
66
+ # smells_details - A hash containing "smell warning" parameters
67
+ #
68
+ # Examples
69
+ #
70
+ # Without smell_details:
71
+ #
72
+ # reek_of(:FeatureEnvy)
73
+ # reek_of(Reek::Smells::UtilityFunction)
74
+ #
75
+ # With smell_details:
76
+ #
77
+ # reek_of(UncommunicativeParameterName, name: 'x2')
78
+ # reek_of(DataClump, count: 3)
79
+ #
80
+ # Examples from a real spec
81
+ #
82
+ # expect(src).to reek_of(Reek::Smells::DuplicateMethodCall, name: '@other.thing')
34
83
  #
35
- def reek_of(smell_category, *patterns)
36
- ShouldReekOf.new(smell_category, patterns)
84
+ def reek_of(smell_category, smell_details = {})
85
+ ShouldReekOf.new(smell_category, smell_details)
37
86
  end
38
87
  end
39
88
  end
@@ -1,7 +1,5 @@
1
1
  require 'reek/examiner'
2
- require 'reek/cli/report/report'
3
2
  require 'reek/cli/report/formatter'
4
- require 'reek/cli/report/strategy'
5
3
 
6
4
  module Reek
7
5
  module Spec
@@ -9,7 +7,7 @@ module Reek
9
7
  # An rspec matcher that matches when the +actual+ has the specified
10
8
  # code smell and no others.
11
9
  #
12
- class ShouldReekOnlyOf < ShouldReekOf # :nodoc:
10
+ class ShouldReekOnlyOf < ShouldReekOf
13
11
  def matches?(actual)
14
12
  matches_examiner?(Examiner.new(actual))
15
13
  end
@@ -17,25 +15,29 @@ module Reek
17
15
  def matches_examiner?(examiner)
18
16
  @examiner = examiner
19
17
  @warnings = @examiner.smells
20
- @warnings.length == 1 && @warnings[0].matches?(@klass, @patterns)
18
+ return false if @warnings.empty?
19
+ @warnings.all? { |warning| warning.matches?(@smell_category) }
21
20
  end
22
21
 
23
22
  def failure_message
24
23
  rpt = Cli::Report::Formatter.format_list(@warnings)
25
- "Expected #{@examiner.description} to reek only of #{@klass}, but got:\n#{rpt}"
24
+ "Expected #{@examiner.description} to reek only of #{@smell_category}, but got:\n#{rpt}"
26
25
  end
27
26
 
28
27
  def failure_message_when_negated
29
- "Expected #{@examiner.description} not to reek only of #{@klass}, but it did"
28
+ "Expected #{@examiner.description} not to reek only of #{@smell_category}, but it did"
30
29
  end
31
30
  end
32
31
 
33
32
  #
34
- # As for reek_of, but the matched smell warning must be the only warning of
35
- # any kind in the target source code's Reek report.
33
+ # See the documentaton for "reek_of".
36
34
  #
37
- def reek_only_of(smell_category, *patterns)
38
- ShouldReekOnlyOf.new(smell_category, patterns)
35
+ # Notable differences to reek_of:
36
+ # 1.) "reek_of" doesn't mind if there are other smells of a different category.
37
+ # "reek_only_of" will fail in that case.
38
+ # 2.) "reek_only_of" doesn't support the additional smell_details hash.
39
+ def reek_only_of(smell_category)
40
+ ShouldReekOnlyOf.new(smell_category)
39
41
  end
40
42
  end
41
43
  end
@@ -1,3 +1,3 @@
1
1
  module Reek
2
- VERSION = '1.6.6'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -30,9 +30,9 @@ Gem::Specification.new do |s|
30
30
  s.required_ruby_version = '>= 1.9.3'
31
31
  s.summary = 'Code smell detector for Ruby'
32
32
 
33
- s.add_runtime_dependency('parser', ['~> 2.2.0.pre.7'])
33
+ s.add_runtime_dependency('parser', ['~> 2.2'])
34
34
  s.add_runtime_dependency('unparser', ['~> 0.2.2'])
35
- s.add_runtime_dependency('rainbow', ['>= 1.99', '< 3.0'])
35
+ s.add_runtime_dependency('rainbow', ['~> 2.0'])
36
36
 
37
37
  s.add_development_dependency('bundler', ['~> 1.1'])
38
38
  s.add_development_dependency('rake', ['~> 10.0'])
@@ -7,10 +7,7 @@ FactoryGirl.define do
7
7
  source 'dummy_file'
8
8
 
9
9
  initialize_with do
10
- # The odd looking const_get is necessary for ruby 1.9.3 compatibility.
11
- Kernel.const_get('Reek').
12
- const_get('Smells').
13
- const_get(smell_type).new(source)
10
+ ::Reek::Smells.const_get(smell_type).new(source)
14
11
  end
15
12
  end
16
13
 
@@ -20,7 +17,7 @@ FactoryGirl.define do
20
17
  context 'self'
21
18
  lines [42]
22
19
  message 'smell warning message'
23
- parameters {}
20
+ parameters { {} }
24
21
 
25
22
  initialize_with do
26
23
  new(smell_detector, context: context,
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'reek/examiner'
3
+ require 'reek/cli/report/report'
4
+
5
+ include Reek
6
+ include Reek::Cli
7
+
8
+ describe Report::HtmlReport do
9
+ let(:instance) { Report::HtmlReport.new }
10
+
11
+ context 'with an empty source' do
12
+ let(:examiner) { Examiner.new('') }
13
+
14
+ before do
15
+ instance.add_examiner examiner
16
+ end
17
+
18
+ it 'has the text 0 total warnings' do
19
+ instance.show
20
+
21
+ file = File.expand_path('../../../../reek.html', __FILE__)
22
+ text = File.read(file)
23
+ File.delete(file)
24
+
25
+ expect(text).to include('0 total warnings')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ require 'reek/cli/options'
4
+
5
+ describe Reek::Cli::OptionInterpreter do
6
+ let(:options) { OpenStruct.new }
7
+ let(:instance) { Reek::Cli::OptionInterpreter.new(options) }
8
+
9
+ describe '#reporter' do
10
+ it 'returns a Report::TextReport instance by default' do
11
+ expect(instance.reporter).to be_instance_of Reek::Cli::Report::TextReport
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+ require 'reek/examiner'
3
+ require 'reek/cli/report/report'
4
+ require 'reek/cli/report/formatter'
5
+ require 'reek/cli/report/heading_formatter'
6
+ require 'rainbow'
7
+
8
+ include Reek
9
+ include Reek::Cli
10
+
11
+ describe Report::TextReport do
12
+ let(:report_options) do
13
+ {
14
+ warning_formatter: Report::SimpleWarningFormatter.new,
15
+ report_formatter: Report::Formatter,
16
+ heading_formatter: Report::HeadingFormatter::Quiet
17
+ }
18
+ end
19
+ let(:instance) { Report::TextReport.new report_options }
20
+
21
+ context 'with a single empty source' do
22
+ before do
23
+ instance.add_examiner Examiner.new('')
24
+ end
25
+
26
+ it 'has an empty quiet_report' do
27
+ expect { instance.show }.to_not output.to_stdout
28
+ end
29
+ end
30
+
31
+ context 'with non smelly files' do
32
+ before do
33
+ instance.add_examiner(Examiner.new('def simple() puts "a" end'))
34
+ instance.add_examiner(Examiner.new('def simple() puts "a" end'))
35
+ end
36
+
37
+ context 'with colors disabled' do
38
+ before :each do
39
+ Rainbow.enabled = false
40
+ end
41
+
42
+ it 'shows total of 0 warnings' do
43
+ expect { instance.show }.to output(/0 total warnings\n\Z/).to_stdout
44
+ end
45
+ end
46
+
47
+ context 'with colors enabled' do
48
+ before :each do
49
+ Rainbow.enabled = true
50
+ end
51
+
52
+ it 'has a footer in color' do
53
+ expect { instance.show }.to output(/\e\[32m0 total warnings\n\e\[0m\Z/).to_stdout
54
+ end
55
+ end
56
+ end
57
+
58
+ context 'with a couple of smells' do
59
+ before do
60
+ instance.add_examiner(Examiner.new('def simple(a) a[3] end'))
61
+ instance.add_examiner(Examiner.new('def simple(a) a[3] end'))
62
+ end
63
+
64
+ context 'with colors disabled' do
65
+ before do
66
+ Rainbow.enabled = false
67
+ end
68
+
69
+ it 'has a heading' do
70
+ expect { instance.show }.to output(/string -- 2 warnings/).to_stdout
71
+ end
72
+
73
+ it 'should mention every smell name' do
74
+ expect { instance.show }.to output(/UncommunicativeParameterName/).to_stdout
75
+ expect { instance.show }.to output(/FeatureEnvy/).to_stdout
76
+ end
77
+ end
78
+
79
+ context 'with colors enabled' do
80
+ before do
81
+ Rainbow.enabled = true
82
+ end
83
+
84
+ it 'has a header in color' do
85
+ expect { instance.show }.
86
+ to output(/\A\e\[36mstring -- \e\[0m\e\[33m2 warning\e\[0m\e\[33ms\e\[0m/).to_stdout
87
+ end
88
+
89
+ it 'has a footer in color' do
90
+ expect { instance.show }.
91
+ to output(/\e\[31m4 total warnings\n\e\[0m\Z/).to_stdout
92
+ end
93
+ end
94
+ end
95
+ end