reek 1.2.6 → 1.2.8
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/.yardopts +10 -0
- data/History.txt +62 -0
- data/README.md +74 -0
- data/bin/reek +2 -2
- data/config/defaults.reek +44 -5
- data/features/api.feature +20 -0
- data/features/masking_smells.feature +60 -42
- data/features/options.feature +7 -2
- data/features/rake_task.feature +29 -22
- data/features/reports.feature +9 -41
- data/features/samples.feature +184 -196
- data/features/stdin.feature +5 -8
- data/features/step_definitions/reek_steps.rb +8 -4
- data/features/support/env.rb +2 -3
- data/features/yaml.feature +85 -0
- data/lib/reek/cli/application.rb +46 -0
- data/lib/reek/cli/command_line.rb +108 -0
- data/lib/reek/cli/help_command.rb +18 -0
- data/lib/reek/cli/reek_command.rb +37 -0
- data/lib/reek/cli/report.rb +58 -0
- data/lib/reek/cli/version_command.rb +19 -0
- data/lib/reek/cli/yaml_command.rb +32 -0
- data/lib/reek/core/code_context.rb +64 -0
- data/lib/reek/core/code_parser.rb +166 -0
- data/lib/reek/core/method_context.rb +84 -0
- data/lib/reek/core/module_context.rb +20 -0
- data/lib/reek/core/object_refs.rb +51 -0
- data/lib/reek/core/singleton_method_context.rb +20 -0
- data/lib/reek/core/smell_configuration.rb +62 -0
- data/lib/reek/core/sniffer.rb +105 -0
- data/lib/reek/core/stop_context.rb +30 -0
- data/lib/reek/core/warning_collector.rb +27 -0
- data/lib/reek/examiner.rb +104 -0
- data/lib/reek/rake/task.rb +142 -0
- data/lib/reek/smell_warning.rb +73 -23
- data/lib/reek/smells/attribute.rb +26 -21
- data/lib/reek/smells/boolean_parameter.rb +38 -0
- data/lib/reek/smells/class_variable.rb +22 -9
- data/lib/reek/smells/control_couple.rb +40 -16
- data/lib/reek/smells/data_clump.rb +114 -23
- data/lib/reek/smells/duplication.rb +55 -15
- data/lib/reek/smells/feature_envy.rb +16 -7
- data/lib/reek/smells/irresponsible_module.rb +37 -0
- data/lib/reek/smells/large_class.rb +37 -19
- data/lib/reek/smells/long_method.rb +20 -9
- data/lib/reek/smells/long_parameter_list.rb +22 -10
- data/lib/reek/smells/long_yield_list.rb +43 -7
- data/lib/reek/smells/nested_iterators.rb +68 -8
- data/lib/reek/smells/simulated_polymorphism.rb +26 -15
- data/lib/reek/smells/smell_detector.rb +29 -51
- data/lib/reek/smells/uncommunicative_method_name.rb +74 -0
- data/lib/reek/smells/uncommunicative_module_name.rb +74 -0
- data/lib/reek/smells/uncommunicative_parameter_name.rb +79 -0
- data/lib/reek/smells/uncommunicative_variable_name.rb +89 -0
- data/lib/reek/smells/utility_function.rb +51 -12
- data/lib/reek/smells.rb +29 -0
- data/lib/reek/source/code_comment.rb +37 -0
- data/lib/reek/source/config_file.rb +72 -0
- data/lib/reek/source/core_extras.rb +46 -0
- data/lib/reek/source/reference_collector.rb +28 -0
- data/lib/reek/source/sexp_formatter.rb +17 -0
- data/lib/reek/source/source_code.rb +44 -0
- data/lib/reek/source/source_file.rb +32 -0
- data/lib/reek/source/source_locator.rb +42 -0
- data/lib/reek/source/tree_dresser.rb +204 -0
- data/lib/reek/source.rb +18 -0
- data/lib/reek/spec/should_reek.rb +31 -0
- data/lib/reek/spec/should_reek_of.rb +37 -0
- data/lib/reek/spec/should_reek_only_of.rb +37 -0
- data/lib/reek/spec.rb +51 -0
- data/lib/reek.rb +8 -4
- data/reek.gemspec +9 -7
- data/spec/matchers/smell_of_matcher.rb +58 -0
- data/spec/reek/{help_command_spec.rb → cli/help_command_spec.rb} +3 -4
- data/spec/reek/cli/reek_command_spec.rb +46 -0
- data/spec/reek/cli/report_spec.rb +30 -0
- data/spec/reek/{version_command_spec.rb → cli/version_command_spec.rb} +3 -3
- data/spec/reek/cli/yaml_command_spec.rb +47 -0
- data/spec/reek/core/code_context_spec.rb +145 -0
- data/spec/reek/{code_parser_spec.rb → core/code_parser_spec.rb} +7 -6
- data/spec/reek/{config_spec.rb → core/config_spec.rb} +2 -6
- data/spec/reek/{method_context_spec.rb → core/method_context_spec.rb} +16 -36
- data/spec/reek/core/module_context_spec.rb +27 -0
- data/spec/reek/{object_refs_spec.rb → core/object_refs_spec.rb} +5 -6
- data/spec/reek/core/singleton_method_context_spec.rb +9 -0
- data/spec/reek/core/smell_configuration_spec.rb +11 -0
- data/spec/reek/core/stop_context_spec.rb +17 -0
- data/spec/reek/core/warning_collector_spec.rb +27 -0
- data/spec/reek/examiner_spec.rb +103 -0
- data/spec/reek/smell_warning_spec.rb +74 -82
- data/spec/reek/smells/attribute_spec.rb +42 -23
- data/spec/reek/smells/behaves_like_variable_detector.rb +2 -2
- data/spec/reek/smells/boolean_parameter_spec.rb +66 -0
- data/spec/reek/smells/class_variable_spec.rb +60 -75
- data/spec/reek/smells/control_couple_spec.rb +37 -32
- data/spec/reek/smells/data_clump_spec.rb +63 -24
- data/spec/reek/smells/duplication_spec.rb +106 -57
- data/spec/reek/smells/feature_envy_spec.rb +113 -92
- data/spec/reek/smells/irresponsible_module_spec.rb +58 -0
- data/spec/reek/smells/large_class_spec.rb +67 -57
- data/spec/reek/smells/long_method_spec.rb +40 -10
- data/spec/reek/smells/long_parameter_list_spec.rb +50 -28
- data/spec/reek/smells/long_yield_list_spec.rb +57 -0
- data/spec/reek/smells/nested_iterators_spec.rb +115 -7
- data/spec/reek/smells/simulated_polymorphism_spec.rb +56 -20
- data/spec/reek/smells/smell_detector_shared.rb +42 -0
- data/spec/reek/smells/uncommunicative_method_name_spec.rb +42 -0
- data/spec/reek/smells/uncommunicative_module_name_spec.rb +66 -0
- data/spec/reek/smells/uncommunicative_parameter_name_spec.rb +71 -0
- data/spec/reek/smells/uncommunicative_variable_name_spec.rb +120 -0
- data/spec/reek/smells/utility_function_spec.rb +50 -4
- data/spec/reek/source/code_comment_spec.rb +82 -0
- data/spec/reek/source/object_source_spec.rb +20 -0
- data/spec/reek/source/reference_collector_spec.rb +53 -0
- data/spec/reek/{adapters/source_spec.rb → source/source_code_spec.rb} +7 -8
- data/spec/reek/source/tree_dresser_spec.rb +270 -0
- data/spec/reek/spec/should_reek_of_spec.rb +76 -0
- data/spec/reek/spec/should_reek_only_of_spec.rb +89 -0
- data/spec/reek/{adapters → spec}/should_reek_spec.rb +8 -32
- data/spec/samples/all_but_one_masked/clean_one.rb +1 -0
- data/spec/samples/all_but_one_masked/dirty.rb +1 -0
- data/spec/samples/all_but_one_masked/masked.reek +5 -1
- data/spec/samples/clean_due_to_masking/clean_one.rb +1 -0
- data/spec/samples/clean_due_to_masking/clean_three.rb +1 -0
- data/spec/samples/clean_due_to_masking/clean_two.rb +1 -0
- data/spec/samples/clean_due_to_masking/dirty_one.rb +1 -1
- data/spec/samples/clean_due_to_masking/dirty_two.rb +1 -1
- data/spec/samples/clean_due_to_masking/masked.reek +5 -1
- data/spec/samples/config/allow_duplication.reek +3 -0
- data/spec/samples/config/deeper_nested_iterators.reek +3 -0
- data/spec/samples/corrupt_config_file/dirty.rb +1 -1
- data/spec/samples/demo/demo.rb +8 -0
- data/spec/samples/empty_config_file/dirty.rb +2 -1
- data/spec/samples/exceptions.reek +1 -1
- data/spec/samples/inline_config/dirty.rb +16 -0
- data/spec/samples/inline_config/masked.reek +7 -0
- data/spec/samples/mask_some/dirty.rb +8 -0
- data/spec/samples/mask_some/some.reek +8 -0
- data/spec/samples/masked/dirty.rb +2 -1
- data/spec/samples/masked/masked.reek +3 -1
- data/spec/samples/mixed_results/clean_one.rb +1 -0
- data/spec/samples/mixed_results/clean_three.rb +1 -0
- data/spec/samples/mixed_results/clean_two.rb +1 -0
- data/spec/samples/mixed_results/dirty_one.rb +1 -0
- data/spec/samples/mixed_results/dirty_two.rb +1 -0
- data/spec/samples/not_quite_masked/dirty.rb +2 -1
- data/spec/samples/not_quite_masked/masked.reek +1 -1
- data/spec/samples/overrides/masked/dirty.rb +2 -1
- data/spec/samples/overrides/masked/lower.reek +3 -1
- data/spec/samples/three_clean_files/clean_one.rb +1 -0
- data/spec/samples/three_clean_files/clean_three.rb +1 -0
- data/spec/samples/three_clean_files/clean_two.rb +1 -0
- data/spec/samples/two_smelly_files/dirty_one.rb +2 -1
- data/spec/samples/two_smelly_files/dirty_two.rb +2 -1
- data/spec/spec_helper.rb +10 -2
- data/tasks/reek.rake +2 -2
- data/tasks/test.rake +12 -3
- metadata +121 -79
- data/README.rdoc +0 -84
- data/features/profile.feature +0 -34
- data/lib/reek/adapters/application.rb +0 -46
- data/lib/reek/adapters/command_line.rb +0 -77
- data/lib/reek/adapters/config_file.rb +0 -31
- data/lib/reek/adapters/core_extras.rb +0 -64
- data/lib/reek/adapters/rake_task.rb +0 -121
- data/lib/reek/adapters/report.rb +0 -86
- data/lib/reek/adapters/source.rb +0 -72
- data/lib/reek/adapters/spec.rb +0 -133
- data/lib/reek/block_context.rb +0 -62
- data/lib/reek/class_context.rb +0 -41
- data/lib/reek/code_context.rb +0 -68
- data/lib/reek/code_parser.rb +0 -203
- data/lib/reek/configuration.rb +0 -57
- data/lib/reek/detector_stack.rb +0 -37
- data/lib/reek/help_command.rb +0 -14
- data/lib/reek/if_context.rb +0 -18
- data/lib/reek/masking_collection.rb +0 -33
- data/lib/reek/method_context.rb +0 -138
- data/lib/reek/module_context.rb +0 -49
- data/lib/reek/name.rb +0 -57
- data/lib/reek/object_refs.rb +0 -49
- data/lib/reek/reek_command.rb +0 -28
- data/lib/reek/sexp_formatter.rb +0 -10
- data/lib/reek/singleton_method_context.rb +0 -26
- data/lib/reek/smells/uncommunicative_name.rb +0 -84
- data/lib/reek/sniffer.rb +0 -177
- data/lib/reek/stop_context.rb +0 -35
- data/lib/reek/tree_dresser.rb +0 -82
- data/lib/reek/version_command.rb +0 -14
- data/lib/reek/yield_call_context.rb +0 -12
- data/spec/reek/adapters/report_spec.rb +0 -31
- data/spec/reek/adapters/should_reek_of_spec.rb +0 -138
- data/spec/reek/adapters/should_reek_only_of_spec.rb +0 -87
- data/spec/reek/block_context_spec.rb +0 -65
- data/spec/reek/class_context_spec.rb +0 -161
- data/spec/reek/code_context_spec.rb +0 -182
- data/spec/reek/configuration_spec.rb +0 -12
- data/spec/reek/if_context_spec.rb +0 -17
- data/spec/reek/masking_collection_spec.rb +0 -236
- data/spec/reek/module_context_spec.rb +0 -46
- data/spec/reek/name_spec.rb +0 -37
- data/spec/reek/object_source_spec.rb +0 -23
- data/spec/reek/reek_command_spec.rb +0 -45
- data/spec/reek/singleton_method_context_spec.rb +0 -16
- data/spec/reek/smells/smell_detector_spec.rb +0 -36
- data/spec/reek/smells/uncommunicative_name_spec.rb +0 -146
- data/spec/reek/sniffer_spec.rb +0 -11
- data/spec/reek/stop_context_spec.rb +0 -33
- data/spec/reek/tree_dresser_spec.rb +0 -20
data/lib/reek/smell_warning.rb
CHANGED
|
@@ -1,20 +1,78 @@
|
|
|
1
|
+
|
|
1
2
|
module Reek
|
|
2
3
|
|
|
3
4
|
#
|
|
4
5
|
# Reports a warning that a smell has been found.
|
|
6
|
+
# This object is essentially a DTO, and therefore contains a :reek:attribute or two.
|
|
5
7
|
#
|
|
6
8
|
class SmellWarning
|
|
9
|
+
|
|
7
10
|
include Comparable
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
MESSAGE_KEY = 'message'
|
|
13
|
+
SUBCLASS_KEY = 'subclass'
|
|
14
|
+
CLASS_KEY = 'class'
|
|
15
|
+
|
|
16
|
+
CONTEXT_KEY = 'context'
|
|
17
|
+
LINES_KEY = 'lines'
|
|
18
|
+
SOURCE_KEY = 'source'
|
|
19
|
+
|
|
20
|
+
ACTIVE_KEY = 'is_active'
|
|
21
|
+
|
|
22
|
+
def initialize(class_name, context, lines, message,
|
|
23
|
+
source = '', subclass_name = '', parameters = {})
|
|
24
|
+
@smell = {
|
|
25
|
+
CLASS_KEY => class_name,
|
|
26
|
+
SUBCLASS_KEY => subclass_name,
|
|
27
|
+
MESSAGE_KEY => message,
|
|
28
|
+
}
|
|
29
|
+
@smell.merge!(parameters)
|
|
30
|
+
@status = {
|
|
31
|
+
ACTIVE_KEY => true
|
|
32
|
+
}
|
|
33
|
+
@location = {
|
|
34
|
+
CONTEXT_KEY => context.to_s,
|
|
35
|
+
LINES_KEY => lines,
|
|
36
|
+
SOURCE_KEY => source
|
|
37
|
+
}
|
|
15
38
|
end
|
|
16
39
|
|
|
17
|
-
|
|
40
|
+
#
|
|
41
|
+
# Details of the smell found, including its class ({CLASS_KEY}),
|
|
42
|
+
# subclass ({SUBCLASS_KEY}) and summary message ({MESSAGE_KEY})
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash{String => String}]
|
|
45
|
+
#
|
|
46
|
+
attr_reader :smell
|
|
47
|
+
|
|
48
|
+
def smell_class() @smell[CLASS_KEY] end
|
|
49
|
+
def subclass() @smell[SUBCLASS_KEY] end
|
|
50
|
+
def message() @smell[MESSAGE_KEY] end
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
# Details of the smell's location, including its context ({CONTEXT_KEY}),
|
|
54
|
+
# the line numbers on which it occurs ({LINES_KEY}) and the source
|
|
55
|
+
# file ({SOURCE_KEY})
|
|
56
|
+
#
|
|
57
|
+
# @return [Hash{String => String, Array<Number>}]
|
|
58
|
+
#
|
|
59
|
+
attr_reader :location
|
|
60
|
+
|
|
61
|
+
def context() @location[CONTEXT_KEY] end
|
|
62
|
+
def lines() @location[LINES_KEY] end
|
|
63
|
+
def source() @location[SOURCE_KEY] end
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
# Details of the smell's status, including whether it is active ({ACTIVE_KEY})
|
|
67
|
+
# (as opposed to being masked by a config file)
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash{String => Boolean}]
|
|
70
|
+
#
|
|
71
|
+
attr_reader :status
|
|
72
|
+
|
|
73
|
+
def is_active() @status[ACTIVE_KEY] end
|
|
74
|
+
|
|
75
|
+
def hash
|
|
18
76
|
sort_key.hash
|
|
19
77
|
end
|
|
20
78
|
|
|
@@ -28,29 +86,21 @@ module Reek
|
|
|
28
86
|
|
|
29
87
|
def contains_all?(patterns)
|
|
30
88
|
rpt = sort_key.to_s
|
|
31
|
-
return patterns.all? {|
|
|
89
|
+
return patterns.all? {|pattern| pattern === rpt}
|
|
32
90
|
end
|
|
33
91
|
|
|
34
|
-
def
|
|
35
|
-
|
|
92
|
+
def matches?(klass, patterns)
|
|
93
|
+
@smell.values.include?(klass.to_s) and contains_all?(patterns)
|
|
36
94
|
end
|
|
37
95
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def report(format)
|
|
41
|
-
format.gsub(/\%s/, smell_name).gsub(/\%c/, @context).gsub(/\%w/, @message).gsub(/\%m/, @is_masked ? '(masked) ' : '')
|
|
96
|
+
def report_on(listener)
|
|
97
|
+
listener.found_smell(self)
|
|
42
98
|
end
|
|
43
99
|
|
|
44
|
-
|
|
45
|
-
if @is_masked
|
|
46
|
-
report.found_masked_smell(self)
|
|
47
|
-
else
|
|
48
|
-
report.found_smell(self)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
100
|
+
protected
|
|
51
101
|
|
|
52
|
-
def
|
|
53
|
-
@smell
|
|
102
|
+
def sort_key
|
|
103
|
+
[@location[CONTEXT_KEY], @smell[MESSAGE_KEY], @smell[CLASS_KEY]]
|
|
54
104
|
end
|
|
55
105
|
end
|
|
56
106
|
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
|
|
2
|
+
require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
|
|
3
|
+
require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'core', 'smell_configuration')
|
|
2
4
|
|
|
3
5
|
module Reek
|
|
4
6
|
module Smells
|
|
@@ -18,43 +20,46 @@ module Reek
|
|
|
18
20
|
#
|
|
19
21
|
class Attribute < SmellDetector
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
SMELL_CLASS = self.name.split(/::/)[-1]
|
|
24
|
+
SMELL_SUBCLASS = SMELL_CLASS
|
|
25
|
+
|
|
26
|
+
ATTRIBUTE_KEY = 'attribute'
|
|
22
27
|
|
|
23
28
|
def self.contexts # :nodoc:
|
|
24
29
|
[:class, :module]
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def self.default_config
|
|
28
|
-
super.adopt(SmellConfiguration::ENABLED_KEY => false)
|
|
33
|
+
super.adopt(Core::SmellConfiguration::ENABLED_KEY => false)
|
|
29
34
|
end
|
|
30
35
|
|
|
31
|
-
def initialize(config = Attribute.default_config)
|
|
32
|
-
super(config)
|
|
36
|
+
def initialize(source, config = Attribute.default_config)
|
|
37
|
+
super(source, config)
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
#
|
|
36
41
|
# Checks whether the given class declares any attributes.
|
|
37
|
-
# Remembers any smells found.
|
|
38
42
|
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
# @return [Array<SmellWarning>]
|
|
44
|
+
#
|
|
45
|
+
def examine_context(ctx)
|
|
46
|
+
attributes_in(ctx).map do |attr, line|
|
|
47
|
+
smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, [line],
|
|
48
|
+
"declares the attribute #{attr}",
|
|
49
|
+
@source, SMELL_SUBCLASS,
|
|
50
|
+
{ATTRIBUTE_KEY => attr.to_s})
|
|
51
|
+
smell
|
|
46
52
|
end
|
|
47
53
|
end
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
def attributes_in(mod)
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def attributes_in(module_ctx)
|
|
54
58
|
result = Set.new
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
attr_defn_methods = [:attr, :attr_reader, :attr_writer, :attr_accessor]
|
|
60
|
+
module_ctx.local_nodes(:call) do |call_node|
|
|
61
|
+
if attr_defn_methods.include?(call_node.method_name)
|
|
62
|
+
call_node.arg_names.each {|arg| result << [arg, call_node.line] }
|
|
58
63
|
end
|
|
59
64
|
end
|
|
60
65
|
result
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
|
|
2
|
+
require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
|
|
3
|
+
|
|
4
|
+
module Reek
|
|
5
|
+
module Smells
|
|
6
|
+
|
|
7
|
+
#
|
|
8
|
+
# A Boolean parameter effectively permits a method's caller
|
|
9
|
+
# to decide which execution path to take. The
|
|
10
|
+
# offending parameter is a kind of Control Couple.
|
|
11
|
+
#
|
|
12
|
+
# Currently Reek can only detect a Boolean parameter when it has a
|
|
13
|
+
# default initializer.
|
|
14
|
+
#
|
|
15
|
+
class BooleanParameter < SmellDetector
|
|
16
|
+
|
|
17
|
+
SMELL_CLASS = 'ControlCouple'
|
|
18
|
+
SMELL_SUBCLASS = self.name.split(/::/)[-1]
|
|
19
|
+
|
|
20
|
+
PARAMETER_KEY = 'parameter'
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Checks whether the given method has any Boolean parameters.
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<SmellWarning>]
|
|
26
|
+
#
|
|
27
|
+
def examine_context(method_ctx)
|
|
28
|
+
method_ctx.parameters.default_assignments.select do |param, value|
|
|
29
|
+
[:true, :false].include?(value[0])
|
|
30
|
+
end.map do |param, value|
|
|
31
|
+
SmellWarning.new(SMELL_CLASS, method_ctx.full_name, [method_ctx.exp.line],
|
|
32
|
+
"has boolean parameter '#{param.to_s}'",
|
|
33
|
+
@source, SMELL_SUBCLASS, {PARAMETER_KEY => param.to_s})
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'set'
|
|
2
|
+
require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
|
|
3
|
+
require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
|
|
2
4
|
|
|
3
5
|
module Reek
|
|
4
6
|
module Smells
|
|
@@ -13,17 +15,26 @@ module Reek
|
|
|
13
15
|
#
|
|
14
16
|
class ClassVariable < SmellDetector
|
|
15
17
|
|
|
18
|
+
SMELL_CLASS = self.name.split(/::/)[-1]
|
|
19
|
+
SMELL_SUBCLASS = SMELL_CLASS
|
|
20
|
+
VARIABLE_KEY = 'variable'
|
|
21
|
+
|
|
16
22
|
def self.contexts # :nodoc:
|
|
17
23
|
[:class, :module]
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
#
|
|
21
27
|
# Checks whether the given class or module declares any class variables.
|
|
22
|
-
# Remembers any smells found.
|
|
23
28
|
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
# @return [Array<SmellWarning>]
|
|
30
|
+
#
|
|
31
|
+
def examine_context(ctx)
|
|
32
|
+
class_variables_in(ctx.exp).map do |attr_name, lines|
|
|
33
|
+
smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, lines,
|
|
34
|
+
"declares the class variable #{attr_name.to_s}",
|
|
35
|
+
@source, SMELL_SUBCLASS,
|
|
36
|
+
{VARIABLE_KEY => attr_name.to_s})
|
|
37
|
+
smell
|
|
27
38
|
end
|
|
28
39
|
end
|
|
29
40
|
|
|
@@ -31,11 +42,13 @@ module Reek
|
|
|
31
42
|
# Collects the names of the class variables declared and/or used
|
|
32
43
|
# in the given module.
|
|
33
44
|
#
|
|
34
|
-
def class_variables_in(
|
|
35
|
-
result =
|
|
36
|
-
collector = proc
|
|
45
|
+
def class_variables_in(ast)
|
|
46
|
+
result = Hash.new {|hash,key| hash[key] = []}
|
|
47
|
+
collector = proc do |cvar_node|
|
|
48
|
+
result[cvar_node.name].push(cvar_node.line)
|
|
49
|
+
end
|
|
37
50
|
[:cvar, :cvasgn, :cvdecl].each do |stmt_type|
|
|
38
|
-
|
|
51
|
+
ast.each_node(stmt_type, [:class, :module], &collector)
|
|
39
52
|
end
|
|
40
53
|
result
|
|
41
54
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
require '
|
|
2
|
-
require '
|
|
3
|
-
require 'reek/sexp_formatter'
|
|
1
|
+
require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
|
|
2
|
+
require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
|
|
4
3
|
|
|
5
4
|
module Reek
|
|
6
5
|
module Smells
|
|
@@ -33,27 +32,52 @@ module Reek
|
|
|
33
32
|
# method probably has more than one responsibility,
|
|
34
33
|
# because it includes at least two different code paths.
|
|
35
34
|
#
|
|
35
|
+
# One possible solution is to use the Strategy Pattern
|
|
36
|
+
# to pass into the callee what must be done. This is
|
|
37
|
+
# not considered to be control coupling because the
|
|
38
|
+
# callee will do the same thing with the strategy,
|
|
39
|
+
# whatever it happens to be. Sometimes in Ruby the
|
|
40
|
+
# strategy may actually just be a block passed in, and
|
|
41
|
+
# that remains next to where the caller invokes it in
|
|
42
|
+
# the source code.
|
|
43
|
+
#
|
|
36
44
|
class ControlCouple < SmellDetector
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
SMELL_CLASS = self.name.split(/::/)[-1]
|
|
47
|
+
SMELL_SUBCLASS = 'ControlParameter'
|
|
48
|
+
PARAMETER_KEY = 'parameter'
|
|
41
49
|
|
|
42
50
|
#
|
|
43
|
-
# Checks whether the given
|
|
44
|
-
#
|
|
51
|
+
# Checks whether the given method chooses its execution path
|
|
52
|
+
# by testing the value of one of its parameters.
|
|
53
|
+
#
|
|
54
|
+
# @return [Array<SmellWarning>]
|
|
45
55
|
#
|
|
46
56
|
def examine_context(ctx)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
control_parameters(ctx).map do |cond, occurs|
|
|
58
|
+
param = cond.format_ruby
|
|
59
|
+
lines = occurs.map {|exp| exp.line}
|
|
60
|
+
smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, lines,
|
|
61
|
+
"is controlled by argument #{param}",
|
|
62
|
+
@source, SMELL_SUBCLASS,
|
|
63
|
+
{PARAMETER_KEY => param})
|
|
64
|
+
smell
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def control_parameters(method_ctx)
|
|
71
|
+
params = method_ctx.exp.parameter_names
|
|
72
|
+
result = Hash.new {|hash,key| hash[key] = []}
|
|
73
|
+
return result if params.empty?
|
|
74
|
+
method_ctx.local_nodes(:if) do |if_node|
|
|
75
|
+
cond = if_node[1]
|
|
76
|
+
if cond[0] == :lvar and params.include?(cond[1])
|
|
77
|
+
result[cond].push(cond)
|
|
55
78
|
end
|
|
56
79
|
end
|
|
80
|
+
result
|
|
57
81
|
end
|
|
58
82
|
end
|
|
59
83
|
end
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
require '
|
|
2
|
-
require '
|
|
3
|
-
require '
|
|
1
|
+
require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
|
|
2
|
+
require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
|
|
3
|
+
require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'source')
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# Extensions to +Array+ needed by Reek.
|
|
7
|
+
#
|
|
8
|
+
class Array
|
|
9
|
+
def intersection
|
|
10
|
+
self.inject { |res, elem| elem & res }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
4
13
|
|
|
5
14
|
module Reek
|
|
6
15
|
module Smells
|
|
@@ -19,42 +28,66 @@ module Reek
|
|
|
19
28
|
#
|
|
20
29
|
class DataClump < SmellDetector
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
SMELL_CLASS = self.name.split(/::/)[-1]
|
|
32
|
+
|
|
33
|
+
METHODS_KEY = 'methods'
|
|
34
|
+
OCCURRENCES_KEY = 'occurrences'
|
|
35
|
+
PARAMETERS_KEY = 'parameters'
|
|
36
|
+
|
|
37
|
+
# @private
|
|
38
|
+
def self.contexts
|
|
23
39
|
[:class, :module]
|
|
24
40
|
end
|
|
25
41
|
|
|
42
|
+
#
|
|
26
43
|
# The name of the config field that sets the maximum allowed
|
|
27
|
-
# copies of any clump.
|
|
44
|
+
# copies of any clump. No group of common parameters will be
|
|
45
|
+
# reported as a DataClump unless there are more than this many
|
|
46
|
+
# methods containing those parameters.
|
|
47
|
+
#
|
|
28
48
|
MAX_COPIES_KEY = 'max_copies'
|
|
29
|
-
|
|
30
49
|
DEFAULT_MAX_COPIES = 2
|
|
31
50
|
|
|
51
|
+
#
|
|
52
|
+
# The name of the config field that sets the minimum clump
|
|
53
|
+
# size. No group of common parameters will be reported as
|
|
54
|
+
# a DataClump unless it contains at least this many parameters.
|
|
55
|
+
#
|
|
32
56
|
MIN_CLUMP_SIZE_KEY = 'min_clump_size'
|
|
33
57
|
DEFAULT_MIN_CLUMP_SIZE = 2
|
|
34
58
|
|
|
35
59
|
def self.default_config
|
|
36
60
|
super.adopt(
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
MAX_COPIES_KEY => DEFAULT_MAX_COPIES,
|
|
62
|
+
MIN_CLUMP_SIZE_KEY => DEFAULT_MIN_CLUMP_SIZE
|
|
39
63
|
)
|
|
40
64
|
end
|
|
41
65
|
|
|
42
|
-
def initialize(config = DataClump.default_config)
|
|
43
|
-
super(config)
|
|
66
|
+
def initialize(source, config = DataClump.default_config)
|
|
67
|
+
super(source, config)
|
|
44
68
|
end
|
|
45
69
|
|
|
46
70
|
#
|
|
47
71
|
# Checks the given class or module for multiple identical parameter sets.
|
|
48
|
-
#
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<SmellWarning>]
|
|
49
74
|
#
|
|
50
75
|
def examine_context(ctx)
|
|
51
|
-
max_copies = value(MAX_COPIES_KEY, ctx, DEFAULT_MAX_COPIES)
|
|
52
|
-
min_clump_size = value(MIN_CLUMP_SIZE_KEY, ctx, DEFAULT_MIN_CLUMP_SIZE)
|
|
53
|
-
MethodGroup.new(ctx, min_clump_size, max_copies).clumps.
|
|
54
|
-
|
|
76
|
+
@max_copies = value(MAX_COPIES_KEY, ctx, DEFAULT_MAX_COPIES)
|
|
77
|
+
@min_clump_size = value(MIN_CLUMP_SIZE_KEY, ctx, DEFAULT_MIN_CLUMP_SIZE)
|
|
78
|
+
MethodGroup.new(ctx, @min_clump_size, @max_copies).clumps.map do |clump, methods|
|
|
79
|
+
SmellWarning.new('DataClump', ctx.full_name,
|
|
80
|
+
methods.map {|meth| meth.line},
|
|
81
|
+
"takes parameters #{DataClump.print_clump(clump)} to #{methods.length} methods",
|
|
82
|
+
@source, 'DataClump', {
|
|
83
|
+
PARAMETERS_KEY => clump.map {|name| name.to_s},
|
|
84
|
+
OCCURRENCES_KEY => methods.length,
|
|
85
|
+
METHODS_KEY => methods.map {|meth| meth.name}
|
|
86
|
+
})
|
|
55
87
|
end
|
|
56
88
|
end
|
|
57
89
|
|
|
90
|
+
# @private
|
|
58
91
|
def self.print_clump(clump)
|
|
59
92
|
"[#{clump.map {|name| name.to_s}.join(', ')}]"
|
|
60
93
|
end
|
|
@@ -62,27 +95,85 @@ module Reek
|
|
|
62
95
|
end
|
|
63
96
|
|
|
64
97
|
# Represents a group of methods
|
|
65
|
-
|
|
98
|
+
# @private
|
|
99
|
+
class MethodGroup
|
|
66
100
|
|
|
67
101
|
def self.intersection_of_parameters_of(methods)
|
|
68
|
-
methods.map {|meth| meth.
|
|
102
|
+
methods.map {|meth| meth.arg_names}.intersection
|
|
69
103
|
end
|
|
70
104
|
|
|
71
105
|
def initialize(ctx, min_clump_size, max_copies)
|
|
72
|
-
@ctx = ctx
|
|
73
106
|
@min_clump_size = min_clump_size
|
|
74
107
|
@max_copies = max_copies
|
|
108
|
+
@candidate_methods = ctx.local_nodes(:defn).select do |meth|
|
|
109
|
+
meth.arg_names.length >= @min_clump_size
|
|
110
|
+
end.map {|defn_node| CandidateMethod.new(defn_node)}
|
|
111
|
+
delete_infrequent_parameters
|
|
112
|
+
delete_small_methods
|
|
75
113
|
end
|
|
76
114
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
clump = MethodGroup.intersection_of_parameters_of(methods)
|
|
115
|
+
def clumps_containing(method, methods, results)
|
|
116
|
+
methods.each do |other_method|
|
|
117
|
+
clump = [method.arg_names, other_method.arg_names].intersection
|
|
81
118
|
if clump.length >= @min_clump_size
|
|
82
|
-
|
|
119
|
+
others = methods.select { |other| clump - other.arg_names == [] }
|
|
120
|
+
results[clump] += [method] + others
|
|
83
121
|
end
|
|
84
122
|
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def collect_clumps_in(methods, results)
|
|
126
|
+
return if methods.length <= @max_copies
|
|
127
|
+
tail = methods[1..-1]
|
|
128
|
+
clumps_containing(methods[0], tail, results)
|
|
129
|
+
collect_clumps_in(tail, results)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def clumps
|
|
133
|
+
results = Hash.new([])
|
|
134
|
+
collect_clumps_in(@candidate_methods, results)
|
|
135
|
+
results.each_key { |key| results[key].uniq! }
|
|
85
136
|
results
|
|
86
137
|
end
|
|
138
|
+
|
|
139
|
+
def delete_small_methods
|
|
140
|
+
@candidate_methods = @candidate_methods.select do |meth|
|
|
141
|
+
meth.arg_names.length >= @min_clump_size
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def delete_infrequent_parameters
|
|
146
|
+
@candidate_methods.each do |meth|
|
|
147
|
+
meth.arg_names.each do |param|
|
|
148
|
+
occurs = @candidate_methods.inject(0) {|sum, cm| cm.arg_names.include?(param) ? sum+1 : sum}
|
|
149
|
+
meth.delete(param) if occurs <= @max_copies
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# A method definition and a copy of its parameters
|
|
156
|
+
# @private
|
|
157
|
+
class CandidateMethod
|
|
158
|
+
def initialize(defn_node)
|
|
159
|
+
@defn = defn_node
|
|
160
|
+
@params = defn_node.arg_names.clone.sort {|first, second| first.to_s <=> second.to_s}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def arg_names
|
|
164
|
+
@params
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def delete(param)
|
|
168
|
+
@params.delete(param)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def line
|
|
172
|
+
@defn.line
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def name
|
|
176
|
+
@defn.name.to_s # BUG: should report the symbols!
|
|
177
|
+
end
|
|
87
178
|
end
|
|
88
179
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
require '
|
|
2
|
-
require '
|
|
3
|
-
require 'reek/sexp_formatter'
|
|
1
|
+
require File.join( File.dirname( File.expand_path(__FILE__)), 'smell_detector')
|
|
2
|
+
require File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'smell_warning')
|
|
4
3
|
|
|
5
4
|
module Reek
|
|
6
5
|
module Smells
|
|
@@ -20,32 +19,73 @@ module Reek
|
|
|
20
19
|
#
|
|
21
20
|
class Duplication < SmellDetector
|
|
22
21
|
|
|
22
|
+
SMELL_CLASS = self.name.split(/::/)[-1]
|
|
23
|
+
SMELL_SUBCLASS = 'DuplicateMethodCall'
|
|
24
|
+
CALL_KEY = 'call'
|
|
25
|
+
OCCURRENCES_KEY = 'occurrences'
|
|
26
|
+
|
|
23
27
|
# The name of the config field that sets the maximum number of
|
|
24
28
|
# identical calls to be permitted within any single method.
|
|
25
29
|
MAX_ALLOWED_CALLS_KEY = 'max_calls'
|
|
26
30
|
|
|
27
31
|
DEFAULT_MAX_CALLS = 1
|
|
28
32
|
|
|
33
|
+
# The name of the config field that sets the names of any
|
|
34
|
+
# methods for which identical calls should be to be permitted
|
|
35
|
+
# within any single method.
|
|
36
|
+
ALLOW_CALLS_KEY = 'allow_calls'
|
|
37
|
+
|
|
38
|
+
DEFAULT_ALLOW_CALLS = []
|
|
39
|
+
|
|
29
40
|
def self.default_config
|
|
30
|
-
super.adopt(
|
|
41
|
+
super.adopt(
|
|
42
|
+
MAX_ALLOWED_CALLS_KEY => DEFAULT_MAX_CALLS,
|
|
43
|
+
ALLOW_CALLS_KEY => DEFAULT_ALLOW_CALLS
|
|
44
|
+
)
|
|
31
45
|
end
|
|
32
46
|
|
|
33
|
-
def initialize(config = Duplication.default_config)
|
|
34
|
-
super(config)
|
|
47
|
+
def initialize(source, config = Duplication.default_config)
|
|
48
|
+
super(source, config)
|
|
35
49
|
end
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
#
|
|
52
|
+
# Looks for duplicate calls within the body of the method +ctx+.
|
|
53
|
+
#
|
|
54
|
+
# @return [Array<SmellWarning>]
|
|
55
|
+
#
|
|
56
|
+
def examine_context(ctx)
|
|
57
|
+
@max_allowed_calls = value(MAX_ALLOWED_CALLS_KEY, ctx, DEFAULT_MAX_CALLS)
|
|
58
|
+
@allow_calls = value(ALLOW_CALLS_KEY, ctx, DEFAULT_ALLOW_CALLS)
|
|
59
|
+
calls(ctx).select do |call_exp, copies|
|
|
60
|
+
copies.length > @max_allowed_calls and not allow_calls?(call_exp.format_ruby)
|
|
61
|
+
end.map do |call_exp, copies|
|
|
62
|
+
occurs = copies.length
|
|
63
|
+
call = call_exp.format_ruby
|
|
64
|
+
multiple = occurs == 2 ? 'twice' : "#{occurs} times"
|
|
65
|
+
smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, copies.map {|exp| exp.line},
|
|
66
|
+
"calls #{call} #{multiple}",
|
|
67
|
+
@source, SMELL_SUBCLASS,
|
|
68
|
+
{CALL_KEY => call, OCCURRENCES_KEY => occurs})
|
|
69
|
+
smell
|
|
42
70
|
end
|
|
43
71
|
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def calls(method_ctx)
|
|
76
|
+
result = Hash.new {|hash,key| hash[key] = []}
|
|
77
|
+
method_ctx.local_nodes(:call) do |call_node|
|
|
78
|
+
next if call_node.method_name == :new
|
|
79
|
+
result[call_node].push(call_node)
|
|
80
|
+
end
|
|
81
|
+
method_ctx.local_nodes(:attrasgn) do |asgn_node|
|
|
82
|
+
result[asgn_node].push(asgn_node) unless asgn_node.args.length < 2
|
|
48
83
|
end
|
|
84
|
+
result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def allow_calls?(method)
|
|
88
|
+
@allow_calls.any? { |allow| /#{allow}/ === method }
|
|
49
89
|
end
|
|
50
90
|
end
|
|
51
91
|
end
|