reek 0.3.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +20 -0
- data/README.txt +4 -80
- data/Rakefile +15 -4
- data/bin/reek +10 -16
- data/config/defaults.reek +53 -0
- data/lib/reek.rb +1 -21
- data/lib/reek/block_context.rb +37 -0
- data/lib/reek/class_context.rb +73 -0
- data/lib/reek/code_context.rb +47 -0
- data/lib/reek/code_parser.rb +204 -0
- data/lib/reek/exceptions.reek +13 -0
- data/lib/reek/if_context.rb +25 -0
- data/lib/reek/method_context.rb +85 -0
- data/lib/reek/module_context.rb +34 -0
- data/lib/reek/name.rb +42 -0
- data/lib/reek/object_refs.rb +3 -6
- data/lib/reek/options.rb +60 -40
- data/lib/reek/rake_task.rb +20 -29
- data/lib/reek/report.rb +16 -27
- data/lib/reek/sexp_formatter.rb +52 -0
- data/lib/reek/singleton_method_context.rb +27 -0
- data/lib/reek/smell_warning.rb +49 -0
- data/lib/reek/smells/control_couple.rb +21 -13
- data/lib/reek/smells/duplication.rb +23 -27
- data/lib/reek/smells/feature_envy.rb +18 -25
- data/lib/reek/smells/large_class.rb +32 -17
- data/lib/reek/smells/long_method.rb +24 -16
- data/lib/reek/smells/long_parameter_list.rb +25 -18
- data/lib/reek/smells/long_yield_list.rb +7 -9
- data/lib/reek/smells/nested_iterators.rb +13 -9
- data/lib/reek/smells/smell_detector.rb +66 -0
- data/lib/reek/smells/smells.rb +71 -10
- data/lib/reek/smells/uncommunicative_name.rb +49 -41
- data/lib/reek/smells/utility_function.rb +18 -18
- data/lib/reek/source.rb +116 -0
- data/lib/reek/spec.rb +146 -0
- data/lib/reek/stop_context.rb +62 -0
- data/lib/reek/yield_call_context.rb +14 -0
- data/reek.gemspec +42 -0
- data/spec/integration/reek_source_spec.rb +20 -0
- data/spec/{script_spec.rb → integration/script_spec.rb} +11 -24
- data/spec/reek/class_context_spec.rb +198 -0
- data/spec/reek/code_context_spec.rb +92 -0
- data/spec/reek/code_parser_spec.rb +44 -0
- data/spec/reek/config_spec.rb +42 -0
- data/spec/reek/if_context_spec.rb +17 -0
- data/spec/reek/method_context_spec.rb +52 -0
- data/spec/reek/module_context_spec.rb +38 -0
- data/spec/reek/options_spec.rb +2 -28
- data/spec/reek/report_spec.rb +6 -40
- data/spec/reek/sexp_formatter_spec.rb +31 -0
- data/spec/reek/singleton_method_context_spec.rb +17 -0
- data/spec/reek/smells/control_couple_spec.rb +10 -18
- data/spec/reek/smells/duplication_spec.rb +53 -32
- data/spec/reek/smells/feature_envy_spec.rb +87 -49
- data/spec/reek/smells/large_class_spec.rb +45 -4
- data/spec/reek/smells/long_method_spec.rb +25 -41
- data/spec/reek/smells/long_parameter_list_spec.rb +30 -76
- data/spec/reek/smells/nested_iterators_spec.rb +19 -29
- data/spec/reek/smells/smell_spec.rb +9 -18
- data/spec/reek/smells/uncommunicative_name_spec.rb +88 -53
- data/spec/reek/smells/utility_function_spec.rb +45 -44
- data/spec/samples/inline_spec.rb +40 -0
- data/spec/samples/optparse_spec.rb +100 -0
- data/spec/samples/redcloth_spec.rb +93 -0
- data/spec/spec_helper.rb +3 -1
- data/tasks/reek.rake +1 -10
- data/tasks/rspec.rake +16 -35
- metadata +43 -46
- data/lib/reek/checker.rb +0 -66
- data/lib/reek/class_checker.rb +0 -25
- data/lib/reek/file_checker.rb +0 -20
- data/lib/reek/method_checker.rb +0 -198
- data/lib/reek/printer.rb +0 -154
- data/lib/reek/smells/smell.rb +0 -56
- data/lib/reek/version.rb +0 -9
- data/setup.rb +0 -1585
- data/spec/integration_spec.rb +0 -30
- data/spec/reek/class_checker_spec.rb +0 -48
- data/spec/reek/method_checker_spec.rb +0 -67
- data/spec/reek/printer_spec.rb +0 -30
- data/spec/reek_source_spec.rb +0 -12
- data/spec/samples/inline.reek +0 -27
- data/spec/samples/optparse.reek +0 -79
- data/spec/samples/optparse/date.rb +0 -17
- data/spec/samples/optparse/shellwords.rb +0 -6
- data/spec/samples/optparse/time.rb +0 -10
- data/spec/samples/optparse/uri.rb +0 -6
- data/spec/samples/optparse/version.rb +0 -70
- data/spec/samples/redcloth.reek +0 -65
- data/tasks/samples.rake +0 -17
- data/website/index.html +0 -71
- data/website/index.txt +0 -40
- data/website/javascripts/rounded_corners_lite.inc.js +0 -285
- data/website/stylesheets/screen.css +0 -138
- data/website/template.rhtml +0 -48
data/lib/reek/report.rb
CHANGED
@@ -1,52 +1,41 @@
|
|
1
|
-
$:.unshift File.dirname(__FILE__)
|
2
|
-
|
3
1
|
require 'set'
|
2
|
+
require 'reek/smells/smell_detector'
|
4
3
|
|
5
4
|
module Reek
|
6
|
-
|
7
|
-
class SortByContext
|
8
|
-
def self.compare(smell1, smell2)
|
9
|
-
smell1.detailed_report <=> smell2.detailed_report
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
class SortBySmell
|
14
|
-
def self.compare(smell1, smell2)
|
15
|
-
smell1.report <=> smell2.report
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
5
|
class Report
|
20
|
-
|
21
|
-
SORT_ORDERS = {
|
22
|
-
:context => SortByContext,
|
23
|
-
:smell => SortBySmell
|
24
|
-
}
|
6
|
+
include Enumerable
|
25
7
|
|
26
8
|
def initialize # :nodoc:
|
27
|
-
@
|
9
|
+
@report = SortedSet.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def each
|
13
|
+
@report.each { |smell| yield smell }
|
28
14
|
end
|
29
15
|
|
30
16
|
def <<(smell) # :nodoc:
|
31
|
-
@
|
17
|
+
@report << smell
|
18
|
+
true
|
32
19
|
end
|
33
20
|
|
34
21
|
def empty? # :nodoc:
|
35
|
-
@
|
22
|
+
@report.empty?
|
36
23
|
end
|
37
24
|
|
38
25
|
def length # :nodoc:
|
39
|
-
@
|
26
|
+
@report.length
|
40
27
|
end
|
28
|
+
|
29
|
+
alias size length
|
41
30
|
|
42
31
|
def [](index) # :nodoc:
|
43
|
-
@
|
32
|
+
@report.to_a[index]
|
44
33
|
end
|
45
34
|
|
46
|
-
# Creates a formatted report of all the
|
35
|
+
# Creates a formatted report of all the +Smells::SmellWarning+ objects recorded in
|
47
36
|
# this report.
|
48
37
|
def to_s
|
49
|
-
@
|
38
|
+
@report.map {|smell| smell.report}.join("\n")
|
50
39
|
end
|
51
40
|
end
|
52
41
|
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Reek
|
2
|
+
class SexpFormatter
|
3
|
+
def self.format(sexp)
|
4
|
+
return sexp.to_s unless Array === sexp
|
5
|
+
first = sexp[1]
|
6
|
+
case sexp[0]
|
7
|
+
when :array
|
8
|
+
format_all(sexp, ', ')
|
9
|
+
when :call
|
10
|
+
meth, args = sexp[2..3]
|
11
|
+
result = format(first)
|
12
|
+
if meth.to_s == '[]'
|
13
|
+
result += (args.nil? ? '[]' : "[#{format(args)}]")
|
14
|
+
else
|
15
|
+
result += ".#{meth}" + (args ? "(#{format(args)})" : '')
|
16
|
+
end
|
17
|
+
result
|
18
|
+
when :colon2
|
19
|
+
format_all(sexp, '::')
|
20
|
+
when :const, :cvar, :dvar
|
21
|
+
format(first)
|
22
|
+
when :dot2
|
23
|
+
format_all(sexp, '..')
|
24
|
+
when :dstr
|
25
|
+
'"' + format_all(sexp, '') + '"'
|
26
|
+
when :evstr
|
27
|
+
"\#\{#{format(first)}\}"
|
28
|
+
when :fcall, :vcall
|
29
|
+
args = sexp[2]
|
30
|
+
result = first.to_s
|
31
|
+
result += "(#{format(args)})" if args
|
32
|
+
result
|
33
|
+
when :iter
|
34
|
+
'block'
|
35
|
+
when :lasgn
|
36
|
+
format_all(sexp, '=')
|
37
|
+
when :nth_ref
|
38
|
+
"$#{first}"
|
39
|
+
when :str
|
40
|
+
first
|
41
|
+
when :xstr
|
42
|
+
"`#{first}`"
|
43
|
+
else
|
44
|
+
sexp[-1].to_s
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.format_all(sexp, glue)
|
49
|
+
sexp[1..-1].map {|arg| format(arg)}.join(glue)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'reek/name'
|
2
|
+
require 'reek/method_context'
|
3
|
+
require 'reek/sexp_formatter'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
class SingletonMethodContext < MethodContext
|
7
|
+
|
8
|
+
def initialize(outer, exp)
|
9
|
+
super(outer, exp, false)
|
10
|
+
@name = Name.new(exp[2])
|
11
|
+
@receiver = SexpFormatter.format(exp[1])
|
12
|
+
record_depends_on_self
|
13
|
+
end
|
14
|
+
|
15
|
+
def envious_receivers
|
16
|
+
[]
|
17
|
+
end
|
18
|
+
|
19
|
+
def outer_name
|
20
|
+
"#{@outer.outer_name}#{@receiver}.#{@name}/"
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"#{@outer.outer_name}#{@receiver}.#{@name}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'reek/options'
|
2
|
+
|
3
|
+
module Reek
|
4
|
+
|
5
|
+
#
|
6
|
+
# Reports a warning that a smell has been found.
|
7
|
+
#
|
8
|
+
class SmellWarning
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
def initialize(smell, context, warning)
|
12
|
+
@smell = smell
|
13
|
+
@context = context
|
14
|
+
@warning = warning
|
15
|
+
end
|
16
|
+
|
17
|
+
def hash # :nodoc:
|
18
|
+
report.hash
|
19
|
+
end
|
20
|
+
|
21
|
+
def <=>(other)
|
22
|
+
report <=> other.report
|
23
|
+
end
|
24
|
+
|
25
|
+
alias eql? <=> # :nodoc:
|
26
|
+
|
27
|
+
#
|
28
|
+
# Returns +true+ only if this is a warning about an instance of
|
29
|
+
# +smell_class+ and its report string matches all of the +patterns+.
|
30
|
+
#
|
31
|
+
def matches?(smell_class, patterns)
|
32
|
+
return false unless smell_class.to_s == @smell.class.class_name
|
33
|
+
rpt = report
|
34
|
+
return patterns.all? {|exp| exp === rpt}
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Returns a copy of the current report format (see +Options+)
|
39
|
+
# in which the following magic tokens have been substituted:
|
40
|
+
#
|
41
|
+
# * %s <-- the name of the smell that was detected
|
42
|
+
# * %c <-- a description of the +CodeContext+ containing the smell
|
43
|
+
# * %w <-- the specific problem that was detected
|
44
|
+
#
|
45
|
+
def report
|
46
|
+
Options[:format].gsub(/\%s/, @smell.smell_name).gsub(/\%c/, @context.to_s).gsub(/\%w/, @warning)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'reek/
|
1
|
+
require 'reek/smells/smell_detector'
|
2
|
+
require 'reek/smell_warning'
|
3
|
+
require 'reek/sexp_formatter'
|
4
4
|
|
5
5
|
module Reek
|
6
6
|
module Smells
|
@@ -33,21 +33,29 @@ module Reek
|
|
33
33
|
# method probably has more than one responsibility,
|
34
34
|
# because it includes at least two different code paths.
|
35
35
|
#
|
36
|
-
class ControlCouple <
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
class ControlCouple < SmellDetector
|
37
|
+
|
38
|
+
def self.contexts # :nodoc:
|
39
|
+
[:if]
|
40
40
|
end
|
41
41
|
|
42
|
-
def
|
43
|
-
|
44
|
-
cond[0] == :lvar and @args.include?(@couple[1])
|
42
|
+
def self.default_config
|
43
|
+
super.adopt(EXCLUDE_KEY => ['initialize'])
|
45
44
|
end
|
46
45
|
|
47
|
-
def
|
48
|
-
|
46
|
+
def initialize(config = ControlCouple.default_config)
|
47
|
+
super
|
49
48
|
end
|
50
|
-
end
|
51
49
|
|
50
|
+
#
|
51
|
+
# Checks whether the given conditional statement relies on a control couple.
|
52
|
+
# Any smells found are added to the +report+.
|
53
|
+
#
|
54
|
+
def examine_context(cond, report)
|
55
|
+
return unless cond.tests_a_parameter?
|
56
|
+
report << SmellWarning.new(self, cond,
|
57
|
+
"is controlled by argument #{SexpFormatter.format(cond.if_expr)}")
|
58
|
+
end
|
59
|
+
end
|
52
60
|
end
|
53
61
|
end
|
@@ -1,7 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'reek/
|
4
|
-
require 'reek/printer'
|
1
|
+
require 'reek/smells/smell_detector'
|
2
|
+
require 'reek/smell_warning'
|
3
|
+
require 'reek/sexp_formatter'
|
5
4
|
|
6
5
|
module Reek
|
7
6
|
module Smells
|
@@ -19,36 +18,33 @@ module Reek
|
|
19
18
|
# @other.thing + @other.thing
|
20
19
|
# end
|
21
20
|
#
|
22
|
-
class Duplication <
|
21
|
+
class Duplication < SmellDetector
|
23
22
|
|
24
|
-
#
|
25
|
-
#
|
26
|
-
|
27
|
-
# and false otherwise.
|
28
|
-
#
|
29
|
-
def self.examine(method, report)
|
30
|
-
look_for_duplicate_calls(method, report)
|
31
|
-
end
|
23
|
+
# The name of the config field that sets the maximum number of
|
24
|
+
# identical calls to be permitted within any single method.
|
25
|
+
MAX_ALLOWED_CALLS_KEY = 'max_calls'
|
32
26
|
|
33
|
-
def self.
|
34
|
-
|
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
|
27
|
+
def self.default_config
|
28
|
+
super.adopt(MAX_ALLOWED_CALLS_KEY => 1)
|
41
29
|
end
|
42
30
|
|
43
|
-
def initialize(
|
44
|
-
super
|
45
|
-
@
|
31
|
+
def initialize(config = Duplication.default_config)
|
32
|
+
super
|
33
|
+
@max_calls = config[MAX_ALLOWED_CALLS_KEY]
|
46
34
|
end
|
47
35
|
|
48
|
-
def
|
49
|
-
|
36
|
+
def examine_context(method, report)
|
37
|
+
smelly_calls(method).each do |call|
|
38
|
+
report << SmellWarning.new(self, method,
|
39
|
+
"calls #{SexpFormatter.format(call)} multiple times")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def smelly_calls(method) # :nodoc:
|
44
|
+
method.calls.select do |key,val|
|
45
|
+
val > @max_calls and key[2] != :new
|
46
|
+
end.map { |call_exp| call_exp[0] }
|
50
47
|
end
|
51
48
|
end
|
52
|
-
|
53
49
|
end
|
54
50
|
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'reek/
|
1
|
+
require 'reek/smells/smell_detector'
|
2
|
+
require 'reek/smell_warning'
|
3
|
+
require 'reek/sexp_formatter'
|
4
4
|
|
5
5
|
module Reek
|
6
6
|
module Smells
|
@@ -32,34 +32,27 @@ module Reek
|
|
32
32
|
# Currently +FeatureEnvy+ reports any method that refers to self less
|
33
33
|
# often than it refers to (ie. send messages to) some other object.
|
34
34
|
#
|
35
|
-
class FeatureEnvy <
|
35
|
+
class FeatureEnvy < SmellDetector
|
36
|
+
|
37
|
+
def self.default_config
|
38
|
+
super.adopt(EXCLUDE_KEY => ['initialize'])
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(config = FeatureEnvy.default_config)
|
42
|
+
super
|
43
|
+
end
|
36
44
|
|
37
45
|
#
|
38
|
-
# Checks whether the given +
|
46
|
+
# Checks whether the given +context+ includes any code fragment that
|
39
47
|
# might "belong" on another class.
|
40
|
-
# Any smells found are added to the +report
|
41
|
-
# and false otherwise.
|
48
|
+
# Any smells found are added to the +report+.
|
42
49
|
#
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
method.refs.max_keys.each do |r|
|
48
|
-
report << new(method, Printer.print(r))
|
49
|
-
smell_found = true
|
50
|
+
def examine_context(context, report)
|
51
|
+
context.envious_receivers.each do |ref|
|
52
|
+
report << SmellWarning.new(self, context,
|
53
|
+
"refers to #{SexpFormatter.format(ref)} more than self")
|
50
54
|
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
55
|
end
|
62
56
|
end
|
63
|
-
|
64
57
|
end
|
65
58
|
end
|
@@ -1,6 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'reek/smells/smell'
|
1
|
+
require 'reek/smells/smell_detector'
|
2
|
+
require 'reek/smell_warning'
|
4
3
|
|
5
4
|
module Reek
|
6
5
|
module Smells
|
@@ -9,27 +8,43 @@ module Reek
|
|
9
8
|
# A Large Class is a class or module that has a large number of
|
10
9
|
# instance variables, methods or lines of code.
|
11
10
|
#
|
12
|
-
# Currently +LargeClass+ only reports classes having more than
|
13
|
-
#
|
11
|
+
# Currently +LargeClass+ only reports classes having more than a
|
12
|
+
# configurable number of methods. This includes public, protected and
|
13
|
+
# private methods, but excludes methods inherited from superclasses or
|
14
|
+
# included modules.
|
14
15
|
#
|
15
|
-
class LargeClass <
|
16
|
-
|
16
|
+
class LargeClass < SmellDetector
|
17
|
+
|
18
|
+
# The name of the config field that sets the maximum number of methods
|
19
|
+
# permitted in a class.
|
20
|
+
MAX_ALLOWED_METHODS_KEY = 'max_methods'
|
17
21
|
|
18
|
-
def self.
|
19
|
-
|
20
|
-
klass.instance_methods - klass.superclass.instance_methods
|
22
|
+
def self.contexts # :nodoc:
|
23
|
+
[:class]
|
21
24
|
end
|
22
25
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
26
|
+
def self.default_config
|
27
|
+
super.adopt(
|
28
|
+
MAX_ALLOWED_METHODS_KEY => 25,
|
29
|
+
EXCLUDE_KEY => ['Array', 'Hash', 'Module', 'String']
|
30
|
+
)
|
27
31
|
end
|
28
32
|
|
29
|
-
def
|
30
|
-
|
33
|
+
def initialize(config = LargeClass.default_config)
|
34
|
+
super
|
35
|
+
@max_methods = config[MAX_ALLOWED_METHODS_KEY]
|
31
36
|
end
|
32
|
-
end
|
33
37
|
|
38
|
+
#
|
39
|
+
# Checks the length of the given +klass+.
|
40
|
+
# Any smells found are added to the +report+.
|
41
|
+
#
|
42
|
+
def examine_context(klass, report)
|
43
|
+
num_methods = klass.num_methods
|
44
|
+
return false if num_methods <= @max_methods
|
45
|
+
report << SmellWarning.new(self, klass,
|
46
|
+
"has at least #{num_methods} methods")
|
47
|
+
end
|
48
|
+
end
|
34
49
|
end
|
35
50
|
end
|