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
@@ -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