kevinrutherford-reek 0.3.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +92 -0
- data/README.txt +6 -0
- data/Rakefile +7 -0
- data/bin/reek +19 -0
- 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 +53 -0
- data/lib/reek/options.rb +92 -0
- data/lib/reek/rake_task.rb +121 -0
- data/lib/reek/report.rb +42 -0
- 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 +61 -0
- data/lib/reek/smells/duplication.rb +50 -0
- data/lib/reek/smells/feature_envy.rb +58 -0
- data/lib/reek/smells/large_class.rb +50 -0
- data/lib/reek/smells/long_method.rb +43 -0
- data/lib/reek/smells/long_parameter_list.rb +43 -0
- data/lib/reek/smells/long_yield_list.rb +18 -0
- data/lib/reek/smells/nested_iterators.rb +28 -0
- data/lib/reek/smells/smell_detector.rb +66 -0
- data/lib/reek/smells/smells.rb +85 -0
- data/lib/reek/smells/uncommunicative_name.rb +80 -0
- data/lib/reek/smells/utility_function.rb +34 -0
- data/lib/reek/source.rb +116 -0
- data/lib/reek/spec.rb +130 -0
- data/lib/reek/stop_context.rb +62 -0
- data/lib/reek/yield_call_context.rb +14 -0
- data/lib/reek.rb +8 -0
- data/spec/integration/reek_source_spec.rb +20 -0
- data/spec/integration/script_spec.rb +55 -0
- 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/module_context_spec.rb +38 -0
- data/spec/reek/object_refs_spec.rb +129 -0
- data/spec/reek/options_spec.rb +13 -0
- data/spec/reek/report_spec.rb +48 -0
- 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 +23 -0
- data/spec/reek/smells/duplication_spec.rb +81 -0
- data/spec/reek/smells/feature_envy_spec.rb +129 -0
- data/spec/reek/smells/large_class_spec.rb +86 -0
- data/spec/reek/smells/long_method_spec.rb +59 -0
- data/spec/reek/smells/long_parameter_list_spec.rb +92 -0
- data/spec/reek/smells/nested_iterators_spec.rb +33 -0
- data/spec/reek/smells/smell_spec.rb +24 -0
- data/spec/reek/smells/uncommunicative_name_spec.rb +118 -0
- data/spec/reek/smells/utility_function_spec.rb +96 -0
- data/spec/samples/inline.rb +704 -0
- data/spec/samples/inline_spec.rb +40 -0
- data/spec/samples/optparse.rb +1788 -0
- data/spec/samples/optparse_spec.rb +100 -0
- data/spec/samples/redcloth.rb +1130 -0
- data/spec/samples/redcloth_spec.rb +93 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +15 -0
- data/tasks/reek.rake +20 -0
- data/tasks/rspec.rake +22 -0
- metadata +167 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sexp_processor'
|
3
|
+
|
4
|
+
module Reek
|
5
|
+
|
6
|
+
class ObjectRefs # :nodoc:
|
7
|
+
def initialize
|
8
|
+
@refs = Hash.new(0)
|
9
|
+
record_reference_to_self
|
10
|
+
end
|
11
|
+
|
12
|
+
def record_reference_to_self
|
13
|
+
record_ref(SELF_REF)
|
14
|
+
end
|
15
|
+
|
16
|
+
def record_ref(exp)
|
17
|
+
type = exp[0]
|
18
|
+
case type
|
19
|
+
when :gvar
|
20
|
+
return
|
21
|
+
when :self
|
22
|
+
record_reference_to_self
|
23
|
+
else
|
24
|
+
@refs[exp] += 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def refs_to_self
|
29
|
+
@refs[SELF_REF]
|
30
|
+
end
|
31
|
+
|
32
|
+
def max_refs
|
33
|
+
@refs.values.max or 0
|
34
|
+
end
|
35
|
+
|
36
|
+
# TODO
|
37
|
+
# Should be moved to Hash; but Hash has 58 methods, and there's currently
|
38
|
+
# no way to turn off that report; which would therefore make the tests fail
|
39
|
+
def max_keys
|
40
|
+
max = max_refs
|
41
|
+
@refs.reject {|key,val| val != max}.keys
|
42
|
+
end
|
43
|
+
|
44
|
+
def self_is_max?
|
45
|
+
max_keys.length == 0 || @refs[SELF_REF] == max_refs
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
SELF_REF = Sexp.from_array([:lit, :self])
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
data/lib/reek/options.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'reek/source'
|
3
|
+
|
4
|
+
module Reek
|
5
|
+
|
6
|
+
class Options
|
7
|
+
|
8
|
+
CTX_SORT = '%c %w (%s)'
|
9
|
+
SMELL_SORT = '[%s] %c %w'
|
10
|
+
|
11
|
+
def self.default_options
|
12
|
+
{
|
13
|
+
:format => CTX_SORT
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
@@opts = default_options
|
18
|
+
|
19
|
+
def self.[](key)
|
20
|
+
@@opts[key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse_args(args)
|
24
|
+
result = default_options
|
25
|
+
parser = OptionParser.new { |opts| set_options(opts, result) }
|
26
|
+
parser.parse!(args)
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.set_options(opts, config)
|
31
|
+
opts.banner = <<EOB
|
32
|
+
Usage: #{opts.program_name} [options] files...
|
33
|
+
|
34
|
+
If no files are given, Reek reads source code from standard input.
|
35
|
+
See http://wiki.github.com/kevinrutherford/reek for detailed help.
|
36
|
+
EOB
|
37
|
+
|
38
|
+
opts.separator "\nOptions:"
|
39
|
+
set_help_option(opts)
|
40
|
+
set_sort_option(config, opts)
|
41
|
+
set_version_option(opts)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.parse(args)
|
45
|
+
begin
|
46
|
+
@@opts = parse_args(args)
|
47
|
+
if ARGV.length > 0
|
48
|
+
return Source.from_pathlist(ARGV)
|
49
|
+
else
|
50
|
+
return Source.from_io($stdin, 'stdin')
|
51
|
+
end
|
52
|
+
rescue OptionParser::ParseError, SystemCallError => err
|
53
|
+
fatal_error(err)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def self.set_version_option(opts)
|
60
|
+
opts.on("-v", "--version", "Show version") do
|
61
|
+
puts "#{opts.program_name} #{Reek::VERSION}"
|
62
|
+
exit(0)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.set_help_option(opts)
|
67
|
+
opts.on("-h", "--help", "Show this message") do
|
68
|
+
puts opts
|
69
|
+
exit(0)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.set_sort_option(config, opts)
|
74
|
+
opts.on('-f', "--format FORMAT", 'Specify the format of smell warnings') do |arg|
|
75
|
+
config[:format] = arg unless arg.nil?
|
76
|
+
end
|
77
|
+
opts.on('-c', '--context-first', "Sort by context; sets the format string to \"#{CTX_SORT}\"") do
|
78
|
+
config[:format] = CTX_SORT
|
79
|
+
end
|
80
|
+
opts.on('-s', '--smell-first', "Sort by smell; sets the format string to \"#{SMELL_SORT}\"") do
|
81
|
+
config[:format] = SMELL_SORT
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.fatal_error(err) # :nodoc:
|
86
|
+
puts "Error: #{err}"
|
87
|
+
puts "Use '-h' for help."
|
88
|
+
exit(1)
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Define a task library for running reek.
|
4
|
+
|
5
|
+
require 'rake'
|
6
|
+
require 'rake/tasklib'
|
7
|
+
|
8
|
+
module Reek
|
9
|
+
|
10
|
+
# A Rake task that runs reek on a set of source files.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# Reek::RakeTask.new do |t|
|
15
|
+
# t.fail_on_error = false
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# This will create a task that can be run with:
|
19
|
+
#
|
20
|
+
# rake reek
|
21
|
+
#
|
22
|
+
# Examples:
|
23
|
+
#
|
24
|
+
# rake reek # checks lib/**/*.rb
|
25
|
+
# rake reek REEK_SRC=just_one_file.rb # checks a single source file
|
26
|
+
# rake reek REEK_OPTS=-s # sorts the report by smell
|
27
|
+
#
|
28
|
+
class RakeTask < ::Rake::TaskLib
|
29
|
+
|
30
|
+
# Name of reek task.
|
31
|
+
# Defaults to :reek.
|
32
|
+
attr_accessor :name
|
33
|
+
|
34
|
+
# Array of directories to be added to $LOAD_PATH before running reek.
|
35
|
+
# Defaults to ['<the absolute path to reek's lib directory>']
|
36
|
+
attr_accessor :libs
|
37
|
+
|
38
|
+
# Glob pattern to match source files.
|
39
|
+
# Setting the REEK_SRC environment variable overrides this.
|
40
|
+
# Defaults to 'lib/**/*.rb'.
|
41
|
+
attr_accessor :source_files
|
42
|
+
|
43
|
+
# String containing commandline options to be passed to Reek.
|
44
|
+
# Setting the REEK_OPTS environment variable overrides this value.
|
45
|
+
# Defaults to ''.
|
46
|
+
attr_accessor :reek_opts
|
47
|
+
|
48
|
+
# Array of commandline options to pass to ruby. Defaults to [].
|
49
|
+
attr_accessor :ruby_opts
|
50
|
+
|
51
|
+
# Whether or not to fail Rake when an error occurs (typically when smells are found).
|
52
|
+
# Defaults to true.
|
53
|
+
attr_accessor :fail_on_error
|
54
|
+
|
55
|
+
# Use verbose output. If this is set to true, the task will print
|
56
|
+
# the reek command to stdout. Defaults to false.
|
57
|
+
attr_accessor :verbose
|
58
|
+
|
59
|
+
# Defines a new task, using the name +name+.
|
60
|
+
def initialize(name = :reek)
|
61
|
+
@name = name
|
62
|
+
@libs = [File.expand_path(File.dirname(__FILE__) + '/../../lib')]
|
63
|
+
@source_files = nil
|
64
|
+
@ruby_opts = []
|
65
|
+
@reek_opts = ''
|
66
|
+
@fail_on_error = true
|
67
|
+
@sort = nil
|
68
|
+
|
69
|
+
yield self if block_given?
|
70
|
+
@source_files ||= 'lib/**/*.rb'
|
71
|
+
define
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def define # :nodoc:
|
77
|
+
desc 'Check for code smells' unless ::Rake.application.last_comment
|
78
|
+
task(name) { run_task }
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def run_task
|
83
|
+
return if source_file_list.empty?
|
84
|
+
cmd = cmd_words.join(' ')
|
85
|
+
puts cmd if @verbose
|
86
|
+
raise('Smells found!') if !system(cmd) and fail_on_error
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.reek_script
|
90
|
+
File.expand_path(File.dirname(__FILE__) + '/../../bin/reek')
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.ruby_exe
|
94
|
+
File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name'])
|
95
|
+
end
|
96
|
+
|
97
|
+
def cmd_words
|
98
|
+
[RakeTask.ruby_exe] +
|
99
|
+
ruby_options +
|
100
|
+
[ %Q|"#{RakeTask.reek_script}"| ] +
|
101
|
+
[sort_option] +
|
102
|
+
source_file_list.collect { |fn| %["#{fn}"] }
|
103
|
+
end
|
104
|
+
|
105
|
+
def ruby_options
|
106
|
+
lib_path = @libs.join(File::PATH_SEPARATOR)
|
107
|
+
@ruby_opts.clone << "-I\"#{lib_path}\""
|
108
|
+
end
|
109
|
+
|
110
|
+
def sort_option
|
111
|
+
ENV['REEK_OPTS'] || @reek_opts
|
112
|
+
end
|
113
|
+
|
114
|
+
def source_file_list # :nodoc:
|
115
|
+
files = ENV['REEK_SRC'] || @source_files
|
116
|
+
return [] unless files
|
117
|
+
return FileList[files]
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
data/lib/reek/report.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'reek/smells/smell_detector'
|
3
|
+
|
4
|
+
module Reek
|
5
|
+
class Report
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize # :nodoc:
|
9
|
+
@report = SortedSet.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def each
|
13
|
+
@report.each { |smell| yield smell }
|
14
|
+
end
|
15
|
+
|
16
|
+
def <<(smell) # :nodoc:
|
17
|
+
@report << smell
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def empty? # :nodoc:
|
22
|
+
@report.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
def length # :nodoc:
|
26
|
+
@report.length
|
27
|
+
end
|
28
|
+
|
29
|
+
alias size length
|
30
|
+
|
31
|
+
def [](index) # :nodoc:
|
32
|
+
@report.to_a[index]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a formatted report of all the +Smells::SmellWarning+ objects recorded in
|
36
|
+
# this report.
|
37
|
+
def to_s
|
38
|
+
@report.map {|smell| smell.report}.join("\n")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -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
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'reek/smells/smell_detector'
|
2
|
+
require 'reek/smell_warning'
|
3
|
+
require 'reek/sexp_formatter'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
#
|
9
|
+
# Control Coupling occurs when a method or block checks the value of
|
10
|
+
# a parameter in order to decide which execution path to take. The
|
11
|
+
# offending parameter is often called a Control Couple.
|
12
|
+
#
|
13
|
+
# A simple example would be the <tt>quoted</tt> parameter
|
14
|
+
# in the following method:
|
15
|
+
#
|
16
|
+
# def write(quoted)
|
17
|
+
# if quoted
|
18
|
+
# write_quoted(@value)
|
19
|
+
# else
|
20
|
+
# puts @value
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Control Coupling is a kind of duplication, because the calling method
|
25
|
+
# already knows which path should be taken.
|
26
|
+
#
|
27
|
+
# Control Coupling reduces the code's flexibility by creating a
|
28
|
+
# dependency between the caller and callee:
|
29
|
+
# any change to the possible values of the controlling parameter must
|
30
|
+
# be reflected on both sides of the call.
|
31
|
+
#
|
32
|
+
# A Control Couple also reveals a loss of simplicity: the called
|
33
|
+
# method probably has more than one responsibility,
|
34
|
+
# because it includes at least two different code paths.
|
35
|
+
#
|
36
|
+
class ControlCouple < SmellDetector
|
37
|
+
|
38
|
+
def self.contexts # :nodoc:
|
39
|
+
[:if]
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.default_config
|
43
|
+
super.adopt(EXCLUDE_KEY => ['initialize'])
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(config = ControlCouple.default_config)
|
47
|
+
super
|
48
|
+
end
|
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
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'reek/smells/smell_detector'
|
2
|
+
require 'reek/smell_warning'
|
3
|
+
require 'reek/sexp_formatter'
|
4
|
+
|
5
|
+
module Reek
|
6
|
+
module Smells
|
7
|
+
|
8
|
+
#
|
9
|
+
# Duplication occurs when two fragments of code look nearly identical,
|
10
|
+
# or when two fragments of code have nearly identical effects
|
11
|
+
# at some conceptual level.
|
12
|
+
#
|
13
|
+
# Currently +Duplication+ checks for repeated identical method calls
|
14
|
+
# within any one method definition. For example, the following method
|
15
|
+
# will report a warning:
|
16
|
+
#
|
17
|
+
# def double_thing()
|
18
|
+
# @other.thing + @other.thing
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
class Duplication < SmellDetector
|
22
|
+
|
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'
|
26
|
+
|
27
|
+
def self.default_config
|
28
|
+
super.adopt(MAX_ALLOWED_CALLS_KEY => 1)
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(config = Duplication.default_config)
|
32
|
+
super
|
33
|
+
@max_calls = config[MAX_ALLOWED_CALLS_KEY]
|
34
|
+
end
|
35
|
+
|
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] }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'reek/smells/smell_detector'
|
2
|
+
require 'reek/smell_warning'
|
3
|
+
require 'reek/sexp_formatter'
|
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 < 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
|
44
|
+
|
45
|
+
#
|
46
|
+
# Checks whether the given +context+ includes any code fragment that
|
47
|
+
# might "belong" on another class.
|
48
|
+
# Any smells found are added to the +report+.
|
49
|
+
#
|
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")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'reek/smells/smell_detector'
|
2
|
+
require 'reek/smell_warning'
|
3
|
+
|
4
|
+
module Reek
|
5
|
+
module Smells
|
6
|
+
|
7
|
+
#
|
8
|
+
# A Large Class is a class or module that has a large number of
|
9
|
+
# instance variables, methods or lines of code.
|
10
|
+
#
|
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.
|
15
|
+
#
|
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'
|
21
|
+
|
22
|
+
def self.contexts # :nodoc:
|
23
|
+
[:class]
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.default_config
|
27
|
+
super.adopt(
|
28
|
+
MAX_ALLOWED_METHODS_KEY => 25,
|
29
|
+
EXCLUDE_KEY => ['Array', 'Hash', 'Module', 'String']
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(config = LargeClass.default_config)
|
34
|
+
super
|
35
|
+
@max_methods = config[MAX_ALLOWED_METHODS_KEY]
|
36
|
+
end
|
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
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'reek/smells/smell_detector'
|
2
|
+
require 'reek/smell_warning'
|
3
|
+
|
4
|
+
module Reek
|
5
|
+
module Smells
|
6
|
+
|
7
|
+
#
|
8
|
+
# A Long Method is any method that has a large number of lines.
|
9
|
+
#
|
10
|
+
# Currently +LongMethod+ reports any method with more than
|
11
|
+
# 5 statements.
|
12
|
+
#
|
13
|
+
class LongMethod < SmellDetector
|
14
|
+
|
15
|
+
# The name of the config field that sets the maximum number of
|
16
|
+
# statements permitted in any method.
|
17
|
+
MAX_ALLOWED_STATEMENTS_KEY = 'max_statements'
|
18
|
+
|
19
|
+
def self.default_config
|
20
|
+
super.adopt(
|
21
|
+
MAX_ALLOWED_STATEMENTS_KEY => 5,
|
22
|
+
EXCLUDE_KEY => ['initialize']
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(config = LongMethod.default_config)
|
27
|
+
super
|
28
|
+
@max_statements = config[MAX_ALLOWED_STATEMENTS_KEY]
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Checks the length of the given +method+.
|
33
|
+
# Any smells found are added to the +report+.
|
34
|
+
#
|
35
|
+
def examine_context(method, report)
|
36
|
+
num = method.num_statements
|
37
|
+
return false if num <= @max_statements
|
38
|
+
report << SmellWarning.new(self, method,
|
39
|
+
"has approx #{num} statements")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|