reek 0.3.1 → 1.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 (96) hide show
  1. data/History.txt +20 -0
  2. data/README.txt +4 -80
  3. data/Rakefile +15 -4
  4. data/bin/reek +10 -16
  5. data/config/defaults.reek +53 -0
  6. data/lib/reek.rb +1 -21
  7. data/lib/reek/block_context.rb +37 -0
  8. data/lib/reek/class_context.rb +73 -0
  9. data/lib/reek/code_context.rb +47 -0
  10. data/lib/reek/code_parser.rb +204 -0
  11. data/lib/reek/exceptions.reek +13 -0
  12. data/lib/reek/if_context.rb +25 -0
  13. data/lib/reek/method_context.rb +85 -0
  14. data/lib/reek/module_context.rb +34 -0
  15. data/lib/reek/name.rb +42 -0
  16. data/lib/reek/object_refs.rb +3 -6
  17. data/lib/reek/options.rb +60 -40
  18. data/lib/reek/rake_task.rb +20 -29
  19. data/lib/reek/report.rb +16 -27
  20. data/lib/reek/sexp_formatter.rb +52 -0
  21. data/lib/reek/singleton_method_context.rb +27 -0
  22. data/lib/reek/smell_warning.rb +49 -0
  23. data/lib/reek/smells/control_couple.rb +21 -13
  24. data/lib/reek/smells/duplication.rb +23 -27
  25. data/lib/reek/smells/feature_envy.rb +18 -25
  26. data/lib/reek/smells/large_class.rb +32 -17
  27. data/lib/reek/smells/long_method.rb +24 -16
  28. data/lib/reek/smells/long_parameter_list.rb +25 -18
  29. data/lib/reek/smells/long_yield_list.rb +7 -9
  30. data/lib/reek/smells/nested_iterators.rb +13 -9
  31. data/lib/reek/smells/smell_detector.rb +66 -0
  32. data/lib/reek/smells/smells.rb +71 -10
  33. data/lib/reek/smells/uncommunicative_name.rb +49 -41
  34. data/lib/reek/smells/utility_function.rb +18 -18
  35. data/lib/reek/source.rb +116 -0
  36. data/lib/reek/spec.rb +146 -0
  37. data/lib/reek/stop_context.rb +62 -0
  38. data/lib/reek/yield_call_context.rb +14 -0
  39. data/reek.gemspec +42 -0
  40. data/spec/integration/reek_source_spec.rb +20 -0
  41. data/spec/{script_spec.rb → integration/script_spec.rb} +11 -24
  42. data/spec/reek/class_context_spec.rb +198 -0
  43. data/spec/reek/code_context_spec.rb +92 -0
  44. data/spec/reek/code_parser_spec.rb +44 -0
  45. data/spec/reek/config_spec.rb +42 -0
  46. data/spec/reek/if_context_spec.rb +17 -0
  47. data/spec/reek/method_context_spec.rb +52 -0
  48. data/spec/reek/module_context_spec.rb +38 -0
  49. data/spec/reek/options_spec.rb +2 -28
  50. data/spec/reek/report_spec.rb +6 -40
  51. data/spec/reek/sexp_formatter_spec.rb +31 -0
  52. data/spec/reek/singleton_method_context_spec.rb +17 -0
  53. data/spec/reek/smells/control_couple_spec.rb +10 -18
  54. data/spec/reek/smells/duplication_spec.rb +53 -32
  55. data/spec/reek/smells/feature_envy_spec.rb +87 -49
  56. data/spec/reek/smells/large_class_spec.rb +45 -4
  57. data/spec/reek/smells/long_method_spec.rb +25 -41
  58. data/spec/reek/smells/long_parameter_list_spec.rb +30 -76
  59. data/spec/reek/smells/nested_iterators_spec.rb +19 -29
  60. data/spec/reek/smells/smell_spec.rb +9 -18
  61. data/spec/reek/smells/uncommunicative_name_spec.rb +88 -53
  62. data/spec/reek/smells/utility_function_spec.rb +45 -44
  63. data/spec/samples/inline_spec.rb +40 -0
  64. data/spec/samples/optparse_spec.rb +100 -0
  65. data/spec/samples/redcloth_spec.rb +93 -0
  66. data/spec/spec_helper.rb +3 -1
  67. data/tasks/reek.rake +1 -10
  68. data/tasks/rspec.rake +16 -35
  69. metadata +43 -46
  70. data/lib/reek/checker.rb +0 -66
  71. data/lib/reek/class_checker.rb +0 -25
  72. data/lib/reek/file_checker.rb +0 -20
  73. data/lib/reek/method_checker.rb +0 -198
  74. data/lib/reek/printer.rb +0 -154
  75. data/lib/reek/smells/smell.rb +0 -56
  76. data/lib/reek/version.rb +0 -9
  77. data/setup.rb +0 -1585
  78. data/spec/integration_spec.rb +0 -30
  79. data/spec/reek/class_checker_spec.rb +0 -48
  80. data/spec/reek/method_checker_spec.rb +0 -67
  81. data/spec/reek/printer_spec.rb +0 -30
  82. data/spec/reek_source_spec.rb +0 -12
  83. data/spec/samples/inline.reek +0 -27
  84. data/spec/samples/optparse.reek +0 -79
  85. data/spec/samples/optparse/date.rb +0 -17
  86. data/spec/samples/optparse/shellwords.rb +0 -6
  87. data/spec/samples/optparse/time.rb +0 -10
  88. data/spec/samples/optparse/uri.rb +0 -6
  89. data/spec/samples/optparse/version.rb +0 -70
  90. data/spec/samples/redcloth.reek +0 -65
  91. data/tasks/samples.rake +0 -17
  92. data/website/index.html +0 -71
  93. data/website/index.txt +0 -40
  94. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  95. data/website/stylesheets/screen.css +0 -138
  96. data/website/template.rhtml +0 -48
data/lib/reek/report.rb CHANGED
@@ -1,52 +1,41 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
1
  require 'set'
2
+ require 'reek/smells/smell_detector'
4
3
 
5
4
  module Reek
6
-
7
- class SortByContext
8
- def self.compare(smell1, smell2)
9
- smell1.detailed_report <=> smell2.detailed_report
10
- end
11
- end
12
-
13
- class SortBySmell
14
- def self.compare(smell1, smell2)
15
- smell1.report <=> smell2.report
16
- end
17
- end
18
-
19
5
  class Report
20
-
21
- SORT_ORDERS = {
22
- :context => SortByContext,
23
- :smell => SortBySmell
24
- }
6
+ include Enumerable
25
7
 
26
8
  def initialize # :nodoc:
27
- @smells = SortedSet.new
9
+ @report = SortedSet.new
10
+ end
11
+
12
+ def each
13
+ @report.each { |smell| yield smell }
28
14
  end
29
15
 
30
16
  def <<(smell) # :nodoc:
31
- @smells << smell
17
+ @report << smell
18
+ true
32
19
  end
33
20
 
34
21
  def empty? # :nodoc:
35
- @smells.empty?
22
+ @report.empty?
36
23
  end
37
24
 
38
25
  def length # :nodoc:
39
- @smells.length
26
+ @report.length
40
27
  end
28
+
29
+ alias size length
41
30
 
42
31
  def [](index) # :nodoc:
43
- @smells.to_a[index]
32
+ @report.to_a[index]
44
33
  end
45
34
 
46
- # Creates a formatted report of all the smells recorded in
35
+ # Creates a formatted report of all the +Smells::SmellWarning+ objects recorded in
47
36
  # this report.
48
37
  def to_s
49
- @smells.map {|smell| smell.report}.join("\n")
38
+ @report.map {|smell| smell.report}.join("\n")
50
39
  end
51
40
  end
52
41
 
@@ -0,0 +1,52 @@
1
+ module Reek
2
+ class SexpFormatter
3
+ def self.format(sexp)
4
+ return sexp.to_s unless Array === sexp
5
+ first = sexp[1]
6
+ case sexp[0]
7
+ when :array
8
+ format_all(sexp, ', ')
9
+ when :call
10
+ meth, args = sexp[2..3]
11
+ result = format(first)
12
+ if meth.to_s == '[]'
13
+ result += (args.nil? ? '[]' : "[#{format(args)}]")
14
+ else
15
+ result += ".#{meth}" + (args ? "(#{format(args)})" : '')
16
+ end
17
+ result
18
+ when :colon2
19
+ format_all(sexp, '::')
20
+ when :const, :cvar, :dvar
21
+ format(first)
22
+ when :dot2
23
+ format_all(sexp, '..')
24
+ when :dstr
25
+ '"' + format_all(sexp, '') + '"'
26
+ when :evstr
27
+ "\#\{#{format(first)}\}"
28
+ when :fcall, :vcall
29
+ args = sexp[2]
30
+ result = first.to_s
31
+ result += "(#{format(args)})" if args
32
+ result
33
+ when :iter
34
+ 'block'
35
+ when :lasgn
36
+ format_all(sexp, '=')
37
+ when :nth_ref
38
+ "$#{first}"
39
+ when :str
40
+ first
41
+ when :xstr
42
+ "`#{first}`"
43
+ else
44
+ sexp[-1].to_s
45
+ end
46
+ end
47
+
48
+ def self.format_all(sexp, glue)
49
+ sexp[1..-1].map {|arg| format(arg)}.join(glue)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,27 @@
1
+ require 'reek/name'
2
+ require 'reek/method_context'
3
+ require 'reek/sexp_formatter'
4
+
5
+ module Reek
6
+ class SingletonMethodContext < MethodContext
7
+
8
+ def initialize(outer, exp)
9
+ super(outer, exp, false)
10
+ @name = Name.new(exp[2])
11
+ @receiver = SexpFormatter.format(exp[1])
12
+ record_depends_on_self
13
+ end
14
+
15
+ def envious_receivers
16
+ []
17
+ end
18
+
19
+ def outer_name
20
+ "#{@outer.outer_name}#{@receiver}.#{@name}/"
21
+ end
22
+
23
+ def to_s
24
+ "#{@outer.outer_name}#{@receiver}.#{@name}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ require 'reek/options'
2
+
3
+ module Reek
4
+
5
+ #
6
+ # Reports a warning that a smell has been found.
7
+ #
8
+ class SmellWarning
9
+ include Comparable
10
+
11
+ def initialize(smell, context, warning)
12
+ @smell = smell
13
+ @context = context
14
+ @warning = warning
15
+ end
16
+
17
+ def hash # :nodoc:
18
+ report.hash
19
+ end
20
+
21
+ def <=>(other)
22
+ report <=> other.report
23
+ end
24
+
25
+ alias eql? <=> # :nodoc:
26
+
27
+ #
28
+ # Returns +true+ only if this is a warning about an instance of
29
+ # +smell_class+ and its report string matches all of the +patterns+.
30
+ #
31
+ def matches?(smell_class, patterns)
32
+ return false unless smell_class.to_s == @smell.class.class_name
33
+ rpt = report
34
+ return patterns.all? {|exp| exp === rpt}
35
+ end
36
+
37
+ #
38
+ # Returns a copy of the current report format (see +Options+)
39
+ # in which the following magic tokens have been substituted:
40
+ #
41
+ # * %s <-- the name of the smell that was detected
42
+ # * %c <-- a description of the +CodeContext+ containing the smell
43
+ # * %w <-- the specific problem that was detected
44
+ #
45
+ def report
46
+ Options[:format].gsub(/\%s/, @smell.smell_name).gsub(/\%c/, @context.to_s).gsub(/\%w/, @warning)
47
+ end
48
+ end
49
+ end
@@ -1,6 +1,6 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
- require 'reek/smells/smell'
1
+ require 'reek/smells/smell_detector'
2
+ require 'reek/smell_warning'
3
+ require 'reek/sexp_formatter'
4
4
 
5
5
  module Reek
6
6
  module Smells
@@ -33,21 +33,29 @@ module Reek
33
33
  # method probably has more than one responsibility,
34
34
  # because it includes at least two different code paths.
35
35
  #
36
- class ControlCouple < Smell
37
- def initialize(context, args)
38
- super
39
- @args = args
36
+ class ControlCouple < SmellDetector
37
+
38
+ def self.contexts # :nodoc:
39
+ [:if]
40
40
  end
41
41
 
42
- def recognise?(cond)
43
- @couple = cond
44
- cond[0] == :lvar and @args.include?(@couple[1])
42
+ def self.default_config
43
+ super.adopt(EXCLUDE_KEY => ['initialize'])
45
44
  end
46
45
 
47
- def detailed_report
48
- "#{@context} is controlled by argument #{Printer.print(@couple)}"
46
+ def initialize(config = ControlCouple.default_config)
47
+ super
49
48
  end
50
- end
51
49
 
50
+ #
51
+ # Checks whether the given conditional statement relies on a control couple.
52
+ # Any smells found are added to the +report+.
53
+ #
54
+ def examine_context(cond, report)
55
+ return unless cond.tests_a_parameter?
56
+ report << SmellWarning.new(self, cond,
57
+ "is controlled by argument #{SexpFormatter.format(cond.if_expr)}")
58
+ end
59
+ end
52
60
  end
53
61
  end
@@ -1,7 +1,6 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
- require 'reek/smells/smell'
4
- require 'reek/printer'
1
+ require 'reek/smells/smell_detector'
2
+ require 'reek/smell_warning'
3
+ require 'reek/sexp_formatter'
5
4
 
6
5
  module Reek
7
6
  module Smells
@@ -19,36 +18,33 @@ module Reek
19
18
  # @other.thing + @other.thing
20
19
  # end
21
20
  #
22
- class Duplication < Smell
21
+ class Duplication < SmellDetector
23
22
 
24
- #
25
- # Checks the given +method+ for duplication.
26
- # Any smells found are added to the +report+; returns true in that case,
27
- # and false otherwise.
28
- #
29
- def self.examine(method, report)
30
- look_for_duplicate_calls(method, report)
31
- end
23
+ # The name of the config field that sets the maximum number of
24
+ # identical calls to be permitted within any single method.
25
+ MAX_ALLOWED_CALLS_KEY = 'max_calls'
32
26
 
33
- def self.look_for_duplicate_calls(method, report) # :nodoc:
34
- smell_reported = false
35
- method.calls.select {|key,val| val > 1}.each do |call_exp|
36
- call = call_exp[0]
37
- report << new(method, call_exp[0]) unless call[2] == :new
38
- smell_reported = true
39
- end
40
- return smell_reported
27
+ def self.default_config
28
+ super.adopt(MAX_ALLOWED_CALLS_KEY => 1)
41
29
  end
42
30
 
43
- def initialize(context, call)
44
- super(context)
45
- @call = call
31
+ def initialize(config = Duplication.default_config)
32
+ super
33
+ @max_calls = config[MAX_ALLOWED_CALLS_KEY]
46
34
  end
47
35
 
48
- def detailed_report
49
- "#{@context} calls #{Printer.print(@call)} more than once"
36
+ def examine_context(method, report)
37
+ smelly_calls(method).each do |call|
38
+ report << SmellWarning.new(self, method,
39
+ "calls #{SexpFormatter.format(call)} multiple times")
40
+ end
41
+ end
42
+
43
+ def smelly_calls(method) # :nodoc:
44
+ method.calls.select do |key,val|
45
+ val > @max_calls and key[2] != :new
46
+ end.map { |call_exp| call_exp[0] }
50
47
  end
51
48
  end
52
-
53
49
  end
54
50
  end
@@ -1,6 +1,6 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
- require 'reek/smells/smell'
1
+ require 'reek/smells/smell_detector'
2
+ require 'reek/smell_warning'
3
+ require 'reek/sexp_formatter'
4
4
 
5
5
  module Reek
6
6
  module Smells
@@ -32,34 +32,27 @@ module Reek
32
32
  # Currently +FeatureEnvy+ reports any method that refers to self less
33
33
  # often than it refers to (ie. send messages to) some other object.
34
34
  #
35
- class FeatureEnvy < Smell
35
+ class FeatureEnvy < SmellDetector
36
+
37
+ def self.default_config
38
+ super.adopt(EXCLUDE_KEY => ['initialize'])
39
+ end
40
+
41
+ def initialize(config = FeatureEnvy.default_config)
42
+ super
43
+ end
36
44
 
37
45
  #
38
- # Checks whether the given +method+ includes any code fragment that
46
+ # Checks whether the given +context+ includes any code fragment that
39
47
  # might "belong" on another class.
40
- # Any smells found are added to the +report+; returns true in that case,
41
- # and false otherwise.
48
+ # Any smells found are added to the +report+.
42
49
  #
43
- def self.examine(method, report)
44
- return false if method.name == 'initialize'
45
- return false if method.refs.self_is_max?
46
- smell_found = false
47
- method.refs.max_keys.each do |r|
48
- report << new(method, Printer.print(r))
49
- smell_found = true
50
+ def examine_context(context, report)
51
+ context.envious_receivers.each do |ref|
52
+ report << SmellWarning.new(self, context,
53
+ "refers to #{SexpFormatter.format(ref)} more than self")
50
54
  end
51
- smell_found
52
- end
53
-
54
- def initialize(context, receiver)
55
- super(context)
56
- @receiver = receiver
57
- end
58
-
59
- def detailed_report
60
- "#{@context} refers to #{@receiver} more than self"
61
55
  end
62
56
  end
63
-
64
57
  end
65
58
  end
@@ -1,6 +1,5 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
- require 'reek/smells/smell'
1
+ require 'reek/smells/smell_detector'
2
+ require 'reek/smell_warning'
4
3
 
5
4
  module Reek
6
5
  module Smells
@@ -9,27 +8,43 @@ module Reek
9
8
  # A Large Class is a class or module that has a large number of
10
9
  # instance variables, methods or lines of code.
11
10
  #
12
- # Currently +LargeClass+ only reports classes having more than
13
- # +MAX_ALLOWED+ public methods.
11
+ # Currently +LargeClass+ only reports classes having more than a
12
+ # configurable number of methods. This includes public, protected and
13
+ # private methods, but excludes methods inherited from superclasses or
14
+ # included modules.
14
15
  #
15
- class LargeClass < Smell
16
- MAX_ALLOWED = 25
16
+ class LargeClass < SmellDetector
17
+
18
+ # The name of the config field that sets the maximum number of methods
19
+ # permitted in a class.
20
+ MAX_ALLOWED_METHODS_KEY = 'max_methods'
17
21
 
18
- def self.non_inherited_methods(klass)
19
- return klass.instance_methods if klass.superclass.nil?
20
- klass.instance_methods - klass.superclass.instance_methods
22
+ def self.contexts # :nodoc:
23
+ [:class]
21
24
  end
22
25
 
23
- def recognise?(name)
24
- klass = Object.const_get(name) rescue return
25
- @num_methods = LargeClass.non_inherited_methods(klass).length
26
- @num_methods > MAX_ALLOWED
26
+ def self.default_config
27
+ super.adopt(
28
+ MAX_ALLOWED_METHODS_KEY => 25,
29
+ EXCLUDE_KEY => ['Array', 'Hash', 'Module', 'String']
30
+ )
27
31
  end
28
32
 
29
- def detailed_report
30
- "#{@context} has #{@num_methods} methods"
33
+ def initialize(config = LargeClass.default_config)
34
+ super
35
+ @max_methods = config[MAX_ALLOWED_METHODS_KEY]
31
36
  end
32
- end
33
37
 
38
+ #
39
+ # Checks the length of the given +klass+.
40
+ # Any smells found are added to the +report+.
41
+ #
42
+ def examine_context(klass, report)
43
+ num_methods = klass.num_methods
44
+ return false if num_methods <= @max_methods
45
+ report << SmellWarning.new(self, klass,
46
+ "has at least #{num_methods} methods")
47
+ end
48
+ end
34
49
  end
35
50
  end