kevinrutherford-reek 0.3.1.4
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 +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
|