reek 0.0.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 ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2008-09-08
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/README.txt ADDED
@@ -0,0 +1,54 @@
1
+ = reek
2
+
3
+ * http://rubyforge.org/projects/reek/
4
+ * http://www.refactoring-workbook.com/reek
5
+ * mailto:kevin@rutherford-software.com
6
+
7
+ == DESCRIPTION:
8
+
9
+ Reek is a tool that examines Ruby classes, modules and methods and
10
+ reports any code smells it finds.
11
+
12
+ == FEATURES/PROBLEMS:
13
+
14
+ * Not many smells checked right now; more coming soon.
15
+ * The current Feature Envy check is probably over zealous.
16
+ * There's no convenient programmer's API just yet.
17
+
18
+ == SYNOPSIS:
19
+
20
+ $ cd my_project/lib
21
+ $ reek
22
+
23
+ == REQUIREMENTS:
24
+
25
+ * ParseTree
26
+
27
+ == INSTALL:
28
+
29
+ * sudo gem install reek
30
+
31
+ == LICENSE:
32
+
33
+ (The MIT License)
34
+
35
+ Copyright (c) Kevin Rutherford, Rutherford Software
36
+
37
+ Permission is hereby granted, free of charge, to any person obtaining
38
+ a copy of this software and associated documentation files (the
39
+ 'Software'), to deal in the Software without restriction, including
40
+ without limitation the rights to use, copy, modify, merge, publish,
41
+ distribute, sublicense, and/or sell copies of the Software, and to
42
+ permit persons to whom the Software is furnished to do so, subject to
43
+ the following conditions:
44
+
45
+ The above copyright notice and this permission notice shall be
46
+ included in all copies or substantial portions of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
49
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
50
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
51
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
52
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
53
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
54
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
5
+
6
+ task :default => :spec
data/bin/reek ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2008-2-17.
4
+ # Copyright (c) 2008. All rights reserved.
5
+
6
+ begin
7
+ require 'rubygems'
8
+ rescue LoadError
9
+ # no rubygems to load, so we fail silently
10
+ end
11
+
12
+ require 'reek'
13
+
14
+ def classes_currently_loaded
15
+ result = []
16
+ ObjectSpace.each_object(Module) { |klass| result << klass }
17
+ result
18
+ end
19
+
20
+ old_classes = classes_currently_loaded
21
+ files = ARGV
22
+ files = Dir['**/*.rb'] if files.empty?
23
+ files.each { |name| require name }
24
+ new_classes = classes_currently_loaded - old_classes
25
+ if new_classes.empty?
26
+ puts 'Nothing to analyse!'
27
+ else
28
+ puts Reek.analyse(*new_classes).to_s
29
+ end
@@ -0,0 +1,40 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'parse_tree'
5
+ require 'sexp_processor'
6
+
7
+ module Reek
8
+
9
+ class Checker < SexpProcessor
10
+ attr_accessor :description
11
+
12
+ def initialize(smells)
13
+ super()
14
+ @require_empty = false
15
+ @smells = smells
16
+ @description = ''
17
+ @unsupported -= [:cfunc]
18
+ end
19
+
20
+ def report(smell)
21
+ @smells << smell
22
+ end
23
+
24
+ def check_source(code)
25
+ check_parse_tree ParseTree.new.parse_tree_for_string(code)
26
+ end
27
+
28
+ def check_object(obj)
29
+ check_parse_tree ParseTree.new.parse_tree(obj)
30
+ end
31
+
32
+ def to_s
33
+ description
34
+ end
35
+
36
+ def check_parse_tree(sexp)
37
+ sexp.each { |exp| process(exp) }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/checker'
4
+ require 'reek/method_checker'
5
+
6
+ module Reek
7
+
8
+ class ClassChecker < Checker
9
+
10
+ def initialize(smells)
11
+ super(smells)
12
+ @description = ''
13
+ end
14
+
15
+ def process_class(exp)
16
+ @description = exp[1].to_s
17
+ superclass = exp[2]
18
+ LargeClass.check(@description, self)
19
+ exp[3..-1].each { |defn| process(defn) } unless superclass == [:const, :Struct]
20
+ s(exp)
21
+ end
22
+
23
+ def process_defn(exp)
24
+ bc = Reek::MethodChecker.new(@smells, @description)
25
+ bc.process(exp)
26
+ s(exp)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,109 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/checker'
4
+ require 'reek/smells'
5
+ require 'set'
6
+
7
+ module Reek
8
+
9
+ class MethodChecker < Checker
10
+
11
+ def initialize(smells, klass_name)
12
+ super(smells)
13
+ @class_name = klass_name
14
+ @description = klass_name
15
+ @top_level_block = true
16
+ @calls = Hash.new(0)
17
+ @lvars = Set.new
18
+ end
19
+
20
+ def process_defn(exp)
21
+ @description = "#{@class_name}##{exp[1]}"
22
+ UncommunicativeName.check(exp[1], self, 'method')
23
+ process(exp[2])
24
+ @lvars.each {|lvar| UncommunicativeName.check(lvar, self, 'local variable') }
25
+ UtilityFunction.check(@calls, self)
26
+ FeatureEnvy.check(@calls, self)
27
+ s(exp)
28
+ end
29
+
30
+ def process_args(exp)
31
+ LongParameterList.check(exp, self)
32
+ exp.each { |arg| UncommunicativeName.check(arg, self, 'parameter') }
33
+ s(exp)
34
+ end
35
+
36
+ def process_attrset(exp)
37
+ @calls[:self] += 1 if /^@/ === exp[1].to_s
38
+ s(exp)
39
+ end
40
+
41
+ def process_iter(exp)
42
+ @top_level_block = false
43
+ exp[1..-1].each { |s| process(s) }
44
+ s(exp)
45
+ end
46
+
47
+ def process_block(exp)
48
+ if @top_level_block
49
+ LongMethod.check(exp, self)
50
+ else
51
+ LongBlock.check(exp, self)
52
+ end
53
+ exp[1..-1].each { |s| process(s) }
54
+ s(exp)
55
+ end
56
+
57
+ def process_yield(exp)
58
+ LongYieldList.check(exp[1], self)
59
+ process(exp[1])
60
+ s(exp)
61
+ end
62
+
63
+ 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
70
+ process(exp[3]) if exp.length > 3
71
+ s(exp)
72
+ end
73
+
74
+ def process_fcall(exp)
75
+ @calls[:self] += 1
76
+ process(exp[2]) if exp.length > 2
77
+ s(exp)
78
+ end
79
+
80
+ def process_cfunc(exp)
81
+ @calls[:self] += 1
82
+ s(exp)
83
+ end
84
+
85
+ def process_vcall(exp)
86
+ @calls[:self] += 1
87
+ s(exp)
88
+ end
89
+
90
+ def process_ivar(exp)
91
+ UncommunicativeName.check(exp[1], self, 'field')
92
+ @calls[:self] += 1
93
+ s(exp)
94
+ end
95
+
96
+ def process_lasgn(exp)
97
+ @lvars << exp[1]
98
+ @calls[s(:lvar, exp[1])] += 1
99
+ process(exp[2])
100
+ s(exp)
101
+ end
102
+
103
+ def process_iasgn(exp)
104
+ @calls[:self] += 1
105
+ process(exp[2])
106
+ s(exp)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,53 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'parse_tree'
5
+ require 'sexp_processor'
6
+
7
+ module Reek
8
+
9
+ class Printer < SexpProcessor
10
+ def self.print(sexp)
11
+ new.print(sexp)
12
+ end
13
+
14
+ def initialize
15
+ super
16
+ @require_empty = false
17
+ @report = ''
18
+ end
19
+
20
+ def print(sexp)
21
+ @report = sexp.inspect
22
+ process(sexp)
23
+ @report
24
+ end
25
+
26
+ def process_lvar(exp)
27
+ @report = exp[1].inspect
28
+ s(exp)
29
+ end
30
+
31
+ def process_dvar(exp)
32
+ @report = exp[1].inspect
33
+ s(exp)
34
+ end
35
+
36
+ def process_gvar(exp)
37
+ @report = exp[1].inspect
38
+ s(exp)
39
+ end
40
+
41
+ def process_const(exp)
42
+ @report = exp[1].inspect
43
+ s(exp)
44
+ end
45
+
46
+ def process_call(exp)
47
+ @report = "#{exp[1]}.#{exp[2]}"
48
+ @report += "(#{exp[3]})" if exp.length > 3
49
+ s(exp)
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,30 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ module Reek
4
+
5
+ class Report
6
+ def initialize
7
+ @smells = []
8
+ end
9
+
10
+ def <<(smell)
11
+ @smells << smell
12
+ end
13
+
14
+ def empty?
15
+ @smells.empty?
16
+ end
17
+
18
+ def length
19
+ @smells.length
20
+ end
21
+
22
+ def [](i)
23
+ @smells[i]
24
+ end
25
+
26
+ def to_s
27
+ @smells.map {|smell| smell.report}.join("\n")
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,159 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/printer'
4
+
5
+ module Reek
6
+
7
+ class Smell
8
+ def self.convert_camel_case(class_name)
9
+ class_name.gsub(/([a-z])([A-Z])/) { |s| "#{$1} #{$2}"}
10
+ end
11
+
12
+ def initialize(context, arg=nil)
13
+ @context = context
14
+ end
15
+
16
+ def self.check(exp, context, arg=nil)
17
+ smell = new(context, arg)
18
+ context.report(smell) if smell.recognise?(exp)
19
+ end
20
+
21
+ def ==(other)
22
+ self.report == other.report
23
+ end
24
+
25
+ def name
26
+ self.class.convert_camel_case(self.class.name.split(/::/)[1])
27
+ end
28
+
29
+ def report
30
+ "[#{name}] #{detailed_report}"
31
+ end
32
+ end
33
+
34
+ class LongParameterList < Smell
35
+ MAX_ALLOWED = 3
36
+
37
+ def count_parameters(exp)
38
+ result = exp.length - 1
39
+ result -= 1 if Array === exp[-1] and exp[-1][0] == :block
40
+ result
41
+ end
42
+
43
+ def recognise?(args)
44
+ count_parameters(args) > MAX_ALLOWED
45
+ end
46
+
47
+ def detailed_report
48
+ "#{@context.to_s} has > #{MAX_ALLOWED} parameters"
49
+ end
50
+ end
51
+
52
+ class LongYieldList < LongParameterList
53
+ def recognise?(args)
54
+ Array === args and args.length > MAX_ALLOWED
55
+ end
56
+
57
+ def detailed_report
58
+ "#{@context} yields > #{MAX_ALLOWED} parameters"
59
+ end
60
+ end
61
+
62
+ class LongMethod < Smell
63
+ MAX_ALLOWED = 5
64
+
65
+ def count_statements(exp)
66
+ result = exp.length - 1
67
+ result -= 1 if Array === exp[1] and exp[1][0] == :args
68
+ result
69
+ end
70
+
71
+ def recognise?(exp)
72
+ count_statements(exp) > MAX_ALLOWED
73
+ end
74
+
75
+ def detailed_report
76
+ "#{@context} has > #{MAX_ALLOWED} statements"
77
+ end
78
+ end
79
+
80
+ class LongBlock < Smell
81
+ MAX_ALLOWED = 5
82
+
83
+ def count_statements(exp)
84
+ result = exp.length - 1
85
+ result -= 1 if Array === exp[1] and exp[1][0] == :args
86
+ result
87
+ end
88
+
89
+ def recognise?(exp)
90
+ count_statements(exp) > MAX_ALLOWED
91
+ end
92
+
93
+ def detailed_report
94
+ "#{@context} has a block with > #{MAX_ALLOWED} statements"
95
+ end
96
+ end
97
+
98
+ class FeatureEnvy < Smell
99
+ def initialize(context, receiver)
100
+ super
101
+ @receiver = receiver
102
+ end
103
+
104
+ def recognise?(calls)
105
+ max = calls.empty? ? 0 : calls.values.max
106
+ mine = calls[:self]
107
+ return false unless max > mine
108
+ receivers = calls.keys.select { |key| calls[key] == max }
109
+ @receiver = receivers.map {|r| Printer.print(r)}.sort.join(' or ')
110
+ return true
111
+ end
112
+
113
+ def detailed_report
114
+ "#{@context} could be moved to #{@receiver}"
115
+ end
116
+ end
117
+
118
+ class UtilityFunction < Smell
119
+ def recognise?(calls)
120
+ calls[:self] == 0
121
+ end
122
+
123
+ def detailed_report
124
+ "#{@context} doesn't depend on instance state"
125
+ end
126
+ end
127
+
128
+ class LargeClass < Smell
129
+ MAX_ALLOWED = 25
130
+
131
+ def recognise?(name)
132
+ kl = Object.const_get(name) rescue return
133
+ num_methods = kl.instance_methods.length - kl.superclass.instance_methods.length
134
+ num_methods > MAX_ALLOWED
135
+ end
136
+
137
+ def detailed_report
138
+ "#{@context} has > #{MAX_ALLOWED} methods"
139
+ end
140
+ end
141
+
142
+ class UncommunicativeName < Smell
143
+ def initialize(context, symbol_type)
144
+ super
145
+ @symbol_type = symbol_type
146
+ end
147
+
148
+ def recognise?(symbol)
149
+ @symbol = symbol.to_s
150
+ return false if @symbol == '*'
151
+ min_len = (/^@/ === @symbol) ? 3 : 2;
152
+ @symbol.length < min_len
153
+ end
154
+
155
+ def detailed_report
156
+ "#{@context} uses the #{@symbol_type} name '#{@symbol}'"
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,9 @@
1
+ module Reek #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ TINY = 1
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
data/lib/reek.rb ADDED
@@ -0,0 +1,16 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'reek/class_checker'
4
+ require 'reek/report'
5
+
6
+ module Reek
7
+
8
+ def self.analyse(*klasses)
9
+ report = Report.new
10
+ klasses.each do |klass|
11
+ ClassChecker.new(report).check_object(klass)
12
+ end
13
+ report
14
+ end
15
+
16
+ end