reek 0.2.0 → 0.2.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.
data/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ == 0.2.1 2008-09-14
2
+
3
+ * Tweaks:
4
+ * Now works from the source code, instead of requiring each named file
5
+ * Added integration tests that run reek on a couple of gems
6
+
1
7
  == 0.2.0 2008-09-10
2
8
 
3
9
  * Minor enhancements:
data/bin/reek CHANGED
@@ -3,31 +3,8 @@
3
3
  # Created on 2008-2-17.
4
4
  # Copyright (c) 2008 Kevin Rutherford, Rutherford Software Ltd. All rights reserved.
5
5
 
6
- begin
7
- require 'rubygems'
8
- rescue LoadError
9
- # no rubygems to load, so we fail silently
10
- end
11
-
12
6
  require 'reek'
13
7
  require 'reek/options'
14
- require 'reek/version'
15
-
16
- def classes_currently_loaded
17
- result = []
18
- ObjectSpace.each_object(Module) { |klass| result << klass }
19
- result
20
- end
21
-
22
- Reek::Options.parse(ARGV)
23
8
 
24
- old_classes = classes_currently_loaded
25
- files = ARGV
26
- files = Dir['**/*.rb'] if files.empty?
27
- files.each { |name| require name }
28
- new_classes = classes_currently_loaded - old_classes
29
- if new_classes.empty?
30
- puts 'Nothing to analyse!'
31
- else
32
- puts Reek.analyse(*new_classes).to_s
33
- end
9
+ files = Reek::Options.parse(ARGV)
10
+ puts Reek.analyse(*files).to_s
data/lib/reek.rb CHANGED
@@ -1,18 +1,19 @@
1
1
  $:.unshift File.dirname(__FILE__)
2
2
 
3
- require 'reek/class_checker'
3
+ require 'reek/file_checker'
4
4
  require 'reek/report'
5
5
 
6
6
  module Reek # :doc:
7
7
 
8
8
  #
9
- # Analyse the given instances of class Class, looking for code smells.
9
+ # Analyse the given source files, looking for code smells.
10
10
  # Returns a +Report+ listing the smells found.
11
11
  #
12
- def self.analyse(*klasses) # :doc:
12
+ def self.analyse(*files) # :doc:
13
13
  report = Report.new
14
- klasses.each do |klass|
15
- ClassChecker.new(report).check_object(klass)
14
+ files.each do |file|
15
+ source = IO.readlines(file).join
16
+ FileChecker.new(report).check_source(source)
16
17
  end
17
18
  report
18
19
  end
@@ -21,8 +21,7 @@ module Reek
21
21
  end
22
22
 
23
23
  def process_defn(exp) # :nodoc:
24
- bc = Reek::MethodChecker.new(@smells, @description)
25
- bc.process(exp)
24
+ Reek::MethodChecker.new(@smells, @description).process(exp)
26
25
  s(exp)
27
26
  end
28
27
  end
@@ -0,0 +1,26 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/checker'
4
+ require 'reek/class_checker'
5
+ require 'reek/method_checker'
6
+
7
+ module Reek
8
+
9
+ class FileChecker < Checker
10
+
11
+ def initialize(report)
12
+ super(report)
13
+ @description = ''
14
+ end
15
+
16
+ def process_class(exp) # :nodoc:
17
+ Reek::ClassChecker.new(@smells).process(exp)
18
+ s(exp)
19
+ end
20
+
21
+ def process_defn(exp) # :nodoc:
22
+ Reek::MethodChecker.new(@smells, @description).process(exp)
23
+ s(exp)
24
+ end
25
+ end
26
+ end
@@ -10,22 +10,17 @@ module Reek
10
10
 
11
11
  def initialize(smells, klass_name)
12
12
  super(smells)
13
- @class_name = klass_name
14
- @description = klass_name
13
+ @class_name = @description = klass_name
15
14
  @calls = Hash.new(0)
16
15
  @lvars = Set.new
17
- @inside_an_iter = false
16
+ @num_statements = 0
18
17
  end
19
18
 
20
19
  def process_defn(exp)
21
- @num_statements = 0
22
20
  @description = "#{@class_name}##{exp[1]}"
23
21
  UncommunicativeName.check(exp[1], self, 'method')
24
22
  process(exp[2])
25
- @lvars.each {|lvar| UncommunicativeName.check(lvar, self, 'local variable') }
26
- UtilityFunction.check(@calls, self)
27
- FeatureEnvy.check(@calls, self)
28
- LongMethod.check(@num_statements, self)
23
+ check_method_properties
29
24
  s(exp)
30
25
  end
31
26
 
@@ -49,7 +44,7 @@ module Reek
49
44
  end
50
45
 
51
46
  def process_block(exp)
52
- @num_statements += count_statements(exp)
47
+ @num_statements += MethodChecker.count_statements(exp)
53
48
  exp[1..-1].each { |s| process(s) }
54
49
  s(exp)
55
50
  end
@@ -61,12 +56,8 @@ module Reek
61
56
  end
62
57
 
63
58
  def process_call(exp)
64
- receiver = process(exp[1])
65
- receiver = receiver[0] if Array === receiver and Array === receiver[0] and receiver.length == 1
66
- if receiver[0] != :gvar
67
- receiver = :self if receiver == s(:self)
68
- @calls[receiver] += 1
69
- end
59
+ record_receiver(exp[1])
60
+ process_actual_parameters(exp[3])
70
61
  process(exp[3]) if exp.length > 3
71
62
  s(exp)
72
63
  end
@@ -107,11 +98,58 @@ module Reek
107
98
  end
108
99
 
109
100
  private
110
-
111
- def count_statements(exp)
101
+
102
+ def self.count_statements(exp)
112
103
  result = exp.length - 1
113
104
  result -= 1 if Array === exp[1] and exp[1][0] == :args
114
105
  result
115
106
  end
107
+
108
+ def self.is_global_variable?(exp)
109
+ Array === exp and exp[0] == :gvar
110
+ end
111
+
112
+ def record_receiver(exp)
113
+ receiver = MethodChecker.unpack_array(process(exp))
114
+ @calls[receiver] += 1 unless MethodChecker.is_global_variable?(receiver)
115
+ end
116
+
117
+ def self.unpack_array(receiver)
118
+ receiver = receiver[0] if Array === receiver and Array === receiver[0] and receiver.length == 1
119
+ receiver = :self if receiver == s(:self)
120
+ receiver
121
+ end
122
+
123
+ def is_override?
124
+ begin
125
+ klass = Object.const_get(@class_name)
126
+ rescue
127
+ return false
128
+ end
129
+ klass.superclass.instance_methods.include?(@description.to_s.split('#')[1])
130
+ end
131
+
132
+ def check_method_properties
133
+ @lvars.each {|lvar| UncommunicativeName.check(lvar, self, 'local variable') }
134
+ @calls[:self] += 1 if is_override?
135
+ UtilityFunction.check(@calls, self)
136
+ FeatureEnvy.check(@calls, self)
137
+ LongMethod.check(@num_statements, self)
138
+ end
139
+
140
+ def process_actual_parameters(exp)
141
+ return unless Array === exp and exp[0] == :array
142
+ exp[1..-1].each do |param|
143
+ if Array === param
144
+ if param.length == 1
145
+ @calls[:self] += 1 if param[0] == :self
146
+ else
147
+ @calls[param] += 1
148
+ end
149
+ else
150
+ @calls[:self] += 1 if param == :self
151
+ end
152
+ end
153
+ end
116
154
  end
117
155
  end
data/lib/reek/options.rb CHANGED
@@ -48,13 +48,21 @@ module Reek
48
48
  result
49
49
  end
50
50
 
51
+ def self.fatal_error(e) # :nodoc:
52
+ puts "Error: #{e}"
53
+ puts "Use '-h' for help."
54
+ exit(1)
55
+ end
56
+
51
57
  def self.parse(args)
52
58
  begin
53
59
  @@opts = parse_args(args)
60
+ files = ARGV
61
+ files = Dir['**/*.rb'] if files.empty?
62
+ fatal_error('no source files specified') if files.empty?
63
+ files
54
64
  rescue OptionParser::ParseError => e
55
- puts "Error: #{e}"
56
- puts "Use '-h' for help."
57
- exit(1)
65
+ fatal_error(e)
58
66
  end
59
67
  end
60
68
  end
data/lib/reek/printer.rb CHANGED
@@ -19,6 +19,7 @@ module Reek
19
19
 
20
20
  def print(sexp)
21
21
  @report = sexp.inspect
22
+ return @report unless Array === sexp
22
23
  begin
23
24
  process(sexp)
24
25
  rescue
data/lib/reek/report.rb CHANGED
@@ -5,13 +5,13 @@ require 'set'
5
5
  module Reek
6
6
 
7
7
  class SortByContext
8
- def compare(smell1, smell2)
8
+ def self.compare(smell1, smell2)
9
9
  smell1.detailed_report <=> smell2.detailed_report
10
10
  end
11
11
  end
12
12
 
13
13
  class SortBySmell
14
- def compare(smell1, smell2)
14
+ def self.compare(smell1, smell2)
15
15
  smell1.report <=> smell2.report
16
16
  end
17
17
  end
@@ -19,8 +19,8 @@ module Reek
19
19
  class Report
20
20
 
21
21
  SORT_ORDERS = {
22
- :context => SortByContext.new,
23
- :smell => SortBySmell.new
22
+ :context => SortByContext,
23
+ :smell => SortBySmell
24
24
  }
25
25
 
26
26
  def initialize # :nodoc:
@@ -39,8 +39,8 @@ module Reek
39
39
  @smells.length
40
40
  end
41
41
 
42
- def [](i) # :nodoc:
43
- @smells.to_a[i]
42
+ def [](index) # :nodoc:
43
+ @smells.to_a[index]
44
44
  end
45
45
 
46
46
  # Creates a formatted report of all the smells recorded in
data/lib/reek/smells.rb CHANGED
@@ -18,7 +18,16 @@ module Reek
18
18
 
19
19
  def self.check(exp, context, arg=nil)
20
20
  smell = new(context, arg)
21
- context.report(smell) if smell.recognise?(exp)
21
+ if smell.recognise?(exp)
22
+ context.report(smell)
23
+ true
24
+ else
25
+ false
26
+ end
27
+ end
28
+
29
+ def recognise?(stuff)
30
+ @context != nil
22
31
  end
23
32
 
24
33
  def hash # :nodoc:
@@ -39,6 +48,8 @@ module Reek
39
48
  "[#{name}] #{detailed_report}"
40
49
  end
41
50
 
51
+ alias inspect report
52
+
42
53
  def to_s
43
54
  report
44
55
  end
@@ -47,28 +58,30 @@ module Reek
47
58
  class LongParameterList < Smell
48
59
  MAX_ALLOWED = 3
49
60
 
50
- def count_parameters(exp)
61
+ def self.count_parameters(exp)
51
62
  result = exp.length - 1
52
63
  result -= 1 if Array === exp[-1] and exp[-1][0] == :block
53
64
  result
54
65
  end
55
66
 
56
67
  def recognise?(args)
57
- count_parameters(args) > MAX_ALLOWED
68
+ @num_params = LongParameterList.count_parameters(args)
69
+ @num_params > MAX_ALLOWED
58
70
  end
59
71
 
60
72
  def detailed_report
61
- "#{@context.to_s} has > #{MAX_ALLOWED} parameters"
73
+ "#{@context.to_s} has #{@num_params} parameters"
62
74
  end
63
75
  end
64
76
 
65
77
  class LongYieldList < LongParameterList
66
78
  def recognise?(args)
67
- Array === args and args.length > MAX_ALLOWED
79
+ @num_params = args.length
80
+ Array === args and @num_params > MAX_ALLOWED
68
81
  end
69
82
 
70
83
  def detailed_report
71
- "#{@context} yields > #{MAX_ALLOWED} parameters"
84
+ "#{@context} yields #{@num_params} parameters"
72
85
  end
73
86
  end
74
87
 
@@ -86,21 +99,28 @@ module Reek
86
99
  end
87
100
 
88
101
  class FeatureEnvy < Smell
89
- def initialize(context, receiver)
90
- super
91
- @receiver = receiver
102
+
103
+ # TODO
104
+ # Should be moved to Hash; but Hash has 58 methods, and there's currently
105
+ # no way to turn off that report; which would therefore make the tests fail
106
+ def self.max_keys(calls)
107
+ max = calls.values.max or return [:self]
108
+ calls.keys.select { |key| calls[key] == max }
109
+ end
110
+
111
+ def initialize(context, *receivers)
112
+ super(context)
113
+ @receivers = receivers
92
114
  end
93
115
 
94
116
  def recognise?(calls)
95
- max = calls.empty? ? 0 : calls.values.max
96
- return false unless max > calls[:self]
97
- receivers = calls.keys.select { |key| calls[key] == max }
98
- @receiver = receivers.map {|r| Printer.print(r)}.sort.join(' and ')
99
- return true
117
+ @receivers = FeatureEnvy.max_keys(calls)
118
+ return !(@receivers.include?(:self))
100
119
  end
101
120
 
102
121
  def detailed_report
103
- "#{@context} uses #{@receiver} more than self"
122
+ receiver = @receivers.map {|r| Printer.print(r)}.sort.join(' and ')
123
+ "#{@context} uses #{receiver} more than self"
104
124
  end
105
125
  end
106
126
 
@@ -118,13 +138,13 @@ module Reek
118
138
  MAX_ALLOWED = 25
119
139
 
120
140
  def recognise?(name)
121
- kl = Object.const_get(name) rescue return
122
- num_methods = kl.instance_methods.length - kl.superclass.instance_methods.length
123
- num_methods > MAX_ALLOWED
141
+ klass = Object.const_get(name) rescue return
142
+ @num_methods = klass.instance_methods.length - klass.superclass.instance_methods.length
143
+ @num_methods > MAX_ALLOWED
124
144
  end
125
145
 
126
146
  def detailed_report
127
- "#{@context} has > #{MAX_ALLOWED} methods"
147
+ "#{@context} has #{@num_methods} methods"
128
148
  end
129
149
  end
130
150
 
data/lib/reek/version.rb CHANGED
@@ -2,7 +2,7 @@ module Reek #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
4
  MINOR = 2
5
- TINY = 0
5
+ TINY = 1
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -0,0 +1,38 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe 'Integration test:' do
4
+ Dir['spec/samples/*.rb'].each do |source|
5
+ describe source do
6
+ before :each do
7
+ @expected = IO.readlines(source.sub(/\.rb/, '.reek'))
8
+ @expected.each {|line| line.chomp!}
9
+ end
10
+
11
+ it 'should report the correct smells' do
12
+ actual = `ruby -Ilib bin/reek #{source} 2>/dev/null`.split(/\n/)
13
+ @expected.zip(actual).each do |p|
14
+ actual = p[1] ? p[1].chomp : p[1]
15
+ actual.should == p[0]
16
+ end
17
+ end
18
+
19
+ it 'should report the correct smells in smell order' do
20
+ actual = `ruby -Ilib bin/reek --sort smell #{source} 2>/dev/null`.split(/\n/)
21
+ @expected.sort.zip(actual).each do |p|
22
+ actual = p[1] ? p[1].chomp : p[1]
23
+ actual.should == p[0]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ describe 'Reek source code:' do
31
+ Dir['lib/**/*.rb'].each do |source|
32
+ describe source do
33
+ it 'should report no smells' do
34
+ `ruby -Ilib bin/reek #{source}`.should == "\n"
35
+ end
36
+ end
37
+ end
38
+ end