reek 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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