reek 0.0.1

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