reek 0.3.0 → 0.3.1

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 (48) hide show
  1. data/History.txt +11 -1
  2. data/README.txt +1 -0
  3. data/lib/reek.rb +8 -9
  4. data/lib/reek/checker.rb +10 -2
  5. data/lib/reek/class_checker.rb +4 -7
  6. data/lib/reek/file_checker.rb +0 -6
  7. data/lib/reek/method_checker.rb +56 -30
  8. data/lib/reek/object_refs.rb +5 -2
  9. data/lib/reek/printer.rb +45 -7
  10. data/lib/reek/rake_task.rb +5 -3
  11. data/lib/reek/smells/control_couple.rb +53 -0
  12. data/lib/reek/smells/duplication.rb +54 -0
  13. data/lib/reek/smells/feature_envy.rb +65 -0
  14. data/lib/reek/smells/large_class.rb +35 -0
  15. data/lib/reek/smells/long_method.rb +35 -0
  16. data/lib/reek/smells/long_parameter_list.rb +36 -0
  17. data/lib/reek/smells/long_yield_list.rb +20 -0
  18. data/lib/reek/smells/nested_iterators.rb +24 -0
  19. data/lib/reek/smells/smell.rb +56 -0
  20. data/lib/reek/smells/smells.rb +24 -0
  21. data/lib/reek/smells/uncommunicative_name.rb +72 -0
  22. data/lib/reek/smells/utility_function.rb +34 -0
  23. data/lib/reek/version.rb +1 -1
  24. data/spec/integration_spec.rb +6 -6
  25. data/spec/reek/printer_spec.rb +21 -21
  26. data/spec/reek/report_spec.rb +5 -5
  27. data/spec/reek/{control_couple_spec.rb → smells/control_couple_spec.rb} +1 -1
  28. data/spec/reek/smells/duplication_spec.rb +60 -0
  29. data/spec/reek/smells/feature_envy_spec.rb +91 -0
  30. data/spec/reek/{large_class_spec.rb → smells/large_class_spec.rb} +8 -8
  31. data/spec/reek/{long_method_spec.rb → smells/long_method_spec.rb} +1 -1
  32. data/spec/reek/{long_parameter_list_spec.rb → smells/long_parameter_list_spec.rb} +1 -1
  33. data/spec/reek/smells/nested_iterators_spec.rb +43 -0
  34. data/spec/reek/{smell_spec.rb → smells/smell_spec.rb} +2 -2
  35. data/spec/reek/smells/uncommunicative_name_spec.rb +83 -0
  36. data/spec/reek/{utility_function_spec.rb → smells/utility_function_spec.rb} +1 -1
  37. data/spec/samples/inline.reek +13 -5
  38. data/spec/samples/optparse.reek +32 -10
  39. data/spec/samples/redcloth.reek +24 -6
  40. data/spec/script_spec.rb +1 -1
  41. data/tasks/reek.rake +9 -0
  42. data/website/index.html +3 -2
  43. data/website/index.txt +3 -1
  44. metadata +24 -12
  45. data/lib/reek/smells.rb +0 -192
  46. data/spec/reek/feature_envy_spec.rb +0 -222
  47. data/spec/reek/nested_iterators_spec.rb +0 -42
  48. data/spec/reek/uncommunicative_name_spec.rb +0 -106
@@ -0,0 +1,54 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+ require 'reek/printer'
5
+
6
+ module Reek
7
+ module Smells
8
+
9
+ #
10
+ # Duplication occurs when two fragments of code look nearly identical,
11
+ # or when two fragments of code have nearly identical effects
12
+ # at some conceptual level.
13
+ #
14
+ # Currently +Duplication+ checks for repeated identical method calls
15
+ # within any one method definition. For example, the following method
16
+ # will report a warning:
17
+ #
18
+ # def double_thing()
19
+ # @other.thing + @other.thing
20
+ # end
21
+ #
22
+ class Duplication < Smell
23
+
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
32
+
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
41
+ end
42
+
43
+ def initialize(context, call)
44
+ super(context)
45
+ @call = call
46
+ end
47
+
48
+ def detailed_report
49
+ "#{@context} calls #{Printer.print(@call)} more than once"
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,65 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ #
9
+ # Feature Envy occurs when a code fragment references another object
10
+ # more often than it references itself, or when several clients do
11
+ # the same series of manipulations on a particular type of object.
12
+ #
13
+ # A simple example would be the following method, which "belongs"
14
+ # on the Item class and not on the Cart class:
15
+ #
16
+ # class Cart
17
+ # def price
18
+ # @item.price + @item.tax
19
+ # end
20
+ # end
21
+ #
22
+ # Feature Envy reduces the code's ability to communicate intent:
23
+ # code that "belongs" on one class but which is located in another
24
+ # can be hard to find, and may upset the "System of Names"
25
+ # in the host class.
26
+ #
27
+ # Feature Envy also affects the design's flexibility: A code fragment
28
+ # that is in the wrong class creates couplings that may not be natural
29
+ # within the application's domain, and creates a loss of cohesion
30
+ # in the unwilling host class.
31
+ #
32
+ # Currently +FeatureEnvy+ reports any method that refers to self less
33
+ # often than it refers to (ie. send messages to) some other object.
34
+ #
35
+ class FeatureEnvy < Smell
36
+
37
+ #
38
+ # Checks whether the given +method+ includes any code fragment that
39
+ # might "belong" on another class.
40
+ # Any smells found are added to the +report+; returns true in that case,
41
+ # and false otherwise.
42
+ #
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
+ 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
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,35 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ #
9
+ # A Large Class is a class or module that has a large number of
10
+ # instance variables, methods or lines of code.
11
+ #
12
+ # Currently +LargeClass+ only reports classes having more than
13
+ # +MAX_ALLOWED+ public methods.
14
+ #
15
+ class LargeClass < Smell
16
+ MAX_ALLOWED = 25
17
+
18
+ def self.non_inherited_methods(klass)
19
+ return klass.instance_methods if klass.superclass.nil?
20
+ klass.instance_methods - klass.superclass.instance_methods
21
+ end
22
+
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
27
+ end
28
+
29
+ def detailed_report
30
+ "#{@context} has #{@num_methods} methods"
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ #
9
+ # A Long Method is any method that has a large number of lines.
10
+ #
11
+ # Currently +LongMethod+ reports any method with more than
12
+ # +MAX_ALLOWED+ statements.
13
+ #
14
+ class LongMethod < Smell
15
+
16
+ MAX_ALLOWED = 5
17
+
18
+ def self.examine(method, report)
19
+ return if method.name == 'initialize'
20
+ num = method.num_statements
21
+ report << new(method, num) if num > MAX_ALLOWED
22
+ end
23
+
24
+ def initialize(context, num)
25
+ super(context)
26
+ @num_stmts = num
27
+ end
28
+
29
+ def detailed_report
30
+ "#{@context} has approx #{@num_stmts} statements"
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ #
9
+ # A Long Parameter List occurs when a method has more than one
10
+ # or two parameters, or when a method yields more than one or
11
+ # two objects to an associated block.
12
+ #
13
+ # Currently +LongParameterList+ reports any method with more
14
+ # than +MAX_ALLOWED+ parameters.
15
+ #
16
+ class LongParameterList < Smell
17
+ MAX_ALLOWED = 3
18
+
19
+ def self.count_parameters(exp)
20
+ result = exp.length - 1
21
+ result -= 1 if Array === exp[-1] and exp[-1][0] == :block
22
+ result
23
+ end
24
+
25
+ def recognise?(args)
26
+ @num_params = LongParameterList.count_parameters(args)
27
+ @num_params > MAX_ALLOWED
28
+ end
29
+
30
+ def detailed_report
31
+ "#{@context.to_s} has #{@num_params} parameters"
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ class LongYieldList < LongParameterList
9
+ def recognise?(args)
10
+ @num_params = args.length
11
+ Array === args and @num_params > MAX_ALLOWED
12
+ end
13
+
14
+ def detailed_report
15
+ "#{@context} yields #{@num_params} parameters"
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ #
9
+ # A Nested Iterator occurs when a block contains another block.
10
+ #
11
+ # +NestedIterators+ reports failing methods only once.
12
+ #
13
+ class NestedIterators < Smell
14
+ def recognise?(already_in_iter)
15
+ already_in_iter && @context
16
+ end
17
+
18
+ def detailed_report
19
+ "#{@context} has nested iterators"
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/options'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ class Smell
9
+ include Comparable
10
+
11
+ def self.convert_camel_case(class_name)
12
+ class_name.gsub(/([a-z])([A-Z])/) { |s| "#{$1} #{$2}"}
13
+ end
14
+
15
+ def initialize(context, arg=nil)
16
+ @context = context
17
+ end
18
+
19
+ def self.check(exp, context, arg=nil)
20
+ smell = new(context, arg)
21
+ return false unless smell.recognise?(exp)
22
+ context.report(smell)
23
+ true
24
+ end
25
+
26
+ def recognise?(stuff)
27
+ @context != nil
28
+ end
29
+
30
+ def hash # :nodoc:
31
+ report.hash
32
+ end
33
+
34
+ def <=>(other) # :nodoc:
35
+ Options[:sort_order].compare(self, other)
36
+ end
37
+
38
+ alias eql? <=>
39
+
40
+ def name
41
+ Smell.convert_camel_case(self.class.name.split(/::/)[2])
42
+ end
43
+
44
+ def report
45
+ "[#{name}] #{detailed_report}"
46
+ end
47
+
48
+ alias inspect report
49
+
50
+ def to_s
51
+ report
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/control_couple'
4
+ require 'reek/smells/duplication'
5
+ require 'reek/smells/feature_envy'
6
+ require 'reek/smells/long_method'
7
+ require 'reek/smells/long_parameter_list'
8
+ require 'reek/smells/long_yield_list'
9
+ require 'reek/smells/nested_iterators'
10
+ require 'reek/smells/uncommunicative_name'
11
+ require 'reek/smells/utility_function'
12
+
13
+ module Reek
14
+
15
+ SMELLS = {
16
+ :defn => [
17
+ Smells::UncommunicativeName,
18
+ Smells::LongMethod,
19
+ Smells::Duplication,
20
+ Smells::UtilityFunction,
21
+ Smells::FeatureEnvy
22
+ ]
23
+ }
24
+ end
@@ -0,0 +1,72 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ #
9
+ # An Uncommunicative Name is a name that doesn't communicate its intent
10
+ # well enough.
11
+ #
12
+ # Poor names make it hard for the reader to build a mental picture
13
+ # of what's going on in the code. They can also be mis-interpreted;
14
+ # and they hurt the flow of reading, because the reader must slow
15
+ # down to interpret the names.
16
+ #
17
+ # Currently +UncommunicativeName+ checks for
18
+ # * 1-character names
19
+ # * names consisting of a single character followed by a number
20
+ #
21
+ class UncommunicativeName < Smell
22
+
23
+ #
24
+ # Checks the given +method+ for uncommunicative method name,
25
+ # parameter names, local variable names and instance variable names.
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
+ smell_reported = consider(method.name, method, report, 'method')
31
+ method.parameters.each do |param|
32
+ smell_reported = consider(param, method, report, 'parameter') || smell_reported
33
+ end
34
+ method.local_variables.each do |lvar|
35
+ smell_reported = consider(lvar, method, report, 'local variable') || smell_reported
36
+ end
37
+ method.instance_variables.each do |ivar|
38
+ smell_reported = consider(ivar, method, report, 'field') || smell_reported
39
+ end
40
+ smell_reported
41
+ end
42
+
43
+ def self.consider(sym, method, report, type) # :nodoc:
44
+ name = sym.to_s
45
+ if is_bad_name?(name)
46
+ report << new(name, method, type)
47
+ return true
48
+ end
49
+ return false
50
+ end
51
+
52
+ def self.is_bad_name?(name)
53
+ return false if name == '*'
54
+ name = name[1..-1] while /^@/ === name
55
+ return true if name.length < 2
56
+ return true if /^.[0-9]$/ === name
57
+ false
58
+ end
59
+
60
+ def initialize(name, context, symbol_type)
61
+ super(context, symbol_type)
62
+ @bad_name = name
63
+ @symbol_type = symbol_type
64
+ end
65
+
66
+ def detailed_report
67
+ "#{@context} uses the #{@symbol_type} name '#{@bad_name}'"
68
+ end
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,34 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/smells/smell'
4
+
5
+ module Reek
6
+ module Smells
7
+
8
+ #
9
+ # A Utility Function is any instance method that has no
10
+ # dependency on the state of the instance.
11
+ #
12
+ class UtilityFunction < Smell
13
+
14
+ #
15
+ # Checks whether the given +method+ is a utility function.
16
+ # Any smells found are added to the +report+; returns true in that case,
17
+ # and false otherwise.
18
+ #
19
+ def self.examine(method, report)
20
+ return false if method.name == 'initialize'
21
+ if method.num_statements > 0 and !method.depends_on_self
22
+ report << new(method)
23
+ true
24
+ end
25
+ false
26
+ end
27
+
28
+ def detailed_report
29
+ "#{@context} doesn't depend on instance state"
30
+ end
31
+ end
32
+
33
+ end
34
+ end