reek 4.4.0 → 4.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/CONTRIBUTING.md +41 -4
- data/README.md +15 -3
- data/defaults.reek +1 -1
- data/docs/Basic-Smell-Options.md +2 -2
- data/docs/Code-Smells.md +4 -0
- data/docs/How-To-Write-New-Detectors.md +116 -0
- data/docs/How-reek-works-internally.md +3 -4
- data/docs/Instance-Variable-Assumption.md +134 -0
- data/docs/Simulated-Polymorphism.md +1 -1
- data/docs/Smell-Suppression.md +6 -6
- data/docs/{style-guide.md → Style-Guide.md} +0 -0
- data/docs/Unused-Private-Method.md +1 -1
- data/docs/YAML-Reports.md +0 -18
- data/features/configuration_files/directory_specific_directives.feature +4 -4
- data/features/configuration_files/unused_private_method.feature +2 -2
- data/features/samples.feature +122 -117
- data/features/smells/subclassed_from_core_class.feature +1 -1
- data/lib/reek/code_comment.rb +13 -4
- data/lib/reek/context/code_context.rb +1 -0
- data/lib/reek/examiner.rb +24 -27
- data/lib/reek/smells/class_variable.rb +1 -1
- data/lib/reek/smells/control_parameter.rb +1 -1
- data/lib/reek/smells/data_clump.rb +1 -1
- data/lib/reek/smells/duplicate_method_call.rb +1 -1
- data/lib/reek/smells/feature_envy.rb +1 -1
- data/lib/reek/smells/instance_variable_assumption.rb +1 -1
- data/lib/reek/smells/prima_donna_method.rb +1 -1
- data/lib/reek/smells/repeated_conditional.rb +1 -1
- data/lib/reek/smells/smell_detector.rb +5 -14
- data/lib/reek/smells/smell_repository.rb +1 -5
- data/lib/reek/smells/smell_warning.rb +6 -8
- data/lib/reek/smells/subclassed_from_core_class.rb +1 -1
- data/lib/reek/smells/uncommunicative_variable_name.rb +22 -12
- data/lib/reek/smells/unused_private_method.rb +1 -1
- data/lib/reek/spec.rb +2 -2
- data/lib/reek/spec/should_reek_of.rb +12 -8
- data/lib/reek/version.rb +1 -1
- data/spec/reek/code_comment_spec.rb +13 -5
- data/spec/reek/examiner_spec.rb +2 -2
- data/spec/reek/smells/attribute_spec.rb +91 -78
- data/spec/reek/smells/boolean_parameter_spec.rb +72 -64
- data/spec/reek/smells/class_variable_spec.rb +81 -68
- data/spec/reek/smells/control_parameter_spec.rb +101 -141
- data/spec/reek/smells/data_clump_spec.rb +94 -149
- data/spec/reek/smells/duplicate_method_call_spec.rb +98 -85
- data/spec/reek/smells/feature_envy_spec.rb +164 -183
- data/spec/reek/smells/instance_variable_assumption_spec.rb +51 -147
- data/spec/reek/smells/irresponsible_module_spec.rb +153 -170
- data/spec/reek/smells/long_parameter_list_spec.rb +44 -88
- data/spec/reek/smells/long_yield_list_spec.rb +41 -41
- data/spec/reek/smells/manual_dispatch_spec.rb +36 -18
- data/spec/reek/smells/module_initialize_spec.rb +31 -33
- data/spec/reek/smells/nested_iterators_spec.rb +189 -183
- data/spec/reek/smells/nil_check_spec.rb +48 -37
- data/spec/reek/smells/prima_donna_method_spec.rb +41 -26
- data/spec/reek/smells/repeated_conditional_spec.rb +75 -87
- data/spec/reek/smells/smell_warning_spec.rb +7 -0
- data/spec/reek/smells/subclassed_from_core_class_spec.rb +37 -112
- data/spec/reek/smells/too_many_constants_spec.rb +109 -199
- data/spec/reek/smells/too_many_instance_variables_spec.rb +105 -128
- data/spec/reek/smells/too_many_methods_spec.rb +38 -62
- data/spec/reek/smells/too_many_statements_spec.rb +69 -45
- data/spec/reek/smells/uncommunicative_method_name_spec.rb +16 -29
- data/spec/reek/smells/uncommunicative_module_name_spec.rb +24 -37
- data/spec/reek/smells/uncommunicative_parameter_name_spec.rb +55 -60
- data/spec/reek/smells/uncommunicative_variable_name_spec.rb +108 -95
- data/spec/reek/smells/unused_parameters_spec.rb +73 -49
- data/spec/reek/smells/unused_private_method_spec.rb +97 -50
- data/spec/reek/smells/utility_function_spec.rb +130 -188
- data/spec/reek/spec/should_reek_of_spec.rb +2 -2
- metadata +6 -7
- data/lib/reek/cli/warning_collector.rb +0 -27
- data/spec/reek/cli/warning_collector_spec.rb +0 -25
- data/spec/reek/smells/smell_detector_shared.rb +0 -29
data/lib/reek/examiner.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require_relative 'context_builder'
|
3
3
|
require_relative 'source/source_code'
|
4
|
-
require_relative 'cli/warning_collector'
|
5
4
|
require_relative 'smells/smell_repository'
|
6
5
|
|
7
6
|
module Reek
|
@@ -9,8 +8,6 @@ module Reek
|
|
9
8
|
# Applies all available smell detectors to a source.
|
10
9
|
#
|
11
10
|
# @public
|
12
|
-
#
|
13
|
-
# :reek:TooManyInstanceVariables: { max_instance_variables: 7 }
|
14
11
|
class Examiner
|
15
12
|
INCOMPREHENSIBLE_SOURCE_TEMPLATE = <<-EOS.freeze
|
16
13
|
!!!
|
@@ -44,19 +41,24 @@ module Reek
|
|
44
41
|
configuration: Configuration::AppConfiguration.default,
|
45
42
|
smell_repository_class: Smells::SmellRepository)
|
46
43
|
@source = Source::SourceCode.from(source)
|
47
|
-
@collector = CLI::WarningCollector.new
|
48
44
|
@smell_types = smell_repository_class.eligible_smell_types(filter_by_smells)
|
49
45
|
@smell_repository = smell_repository_class.new(smell_types: @smell_types,
|
50
46
|
configuration: configuration.directive_for(description))
|
51
47
|
end
|
52
48
|
|
53
|
-
#
|
49
|
+
# @return [String] origin of the source being analysed
|
54
50
|
#
|
51
|
+
# @public
|
52
|
+
def origin
|
53
|
+
@origin ||= source.origin
|
54
|
+
end
|
55
|
+
|
55
56
|
# @return [String] description of the source being analysed
|
56
57
|
#
|
57
58
|
# @public
|
59
|
+
# @deprecated Use origin
|
58
60
|
def description
|
59
|
-
|
61
|
+
origin
|
60
62
|
end
|
61
63
|
|
62
64
|
#
|
@@ -64,8 +66,7 @@ module Reek
|
|
64
66
|
#
|
65
67
|
# @public
|
66
68
|
def smells
|
67
|
-
run
|
68
|
-
@smells ||= collector.warnings
|
69
|
+
@smells ||= run.sort.uniq
|
69
70
|
end
|
70
71
|
|
71
72
|
#
|
@@ -86,35 +87,31 @@ module Reek
|
|
86
87
|
|
87
88
|
private
|
88
89
|
|
89
|
-
attr_reader :
|
90
|
+
attr_reader :source, :smell_repository
|
90
91
|
|
91
|
-
# Runs the Examiner on the given source to scan for code smells
|
92
|
-
# and returns the corresponding Examiner instance.
|
92
|
+
# Runs the Examiner on the given source to scan for code smells.
|
93
93
|
#
|
94
94
|
# In case one of the smell detectors raises an exception we probably hit a Reek bug.
|
95
95
|
# So we catch the exception here, let the user know something went wrong
|
96
96
|
# and continue with the analysis.
|
97
97
|
#
|
98
|
-
# @return
|
99
|
-
#
|
100
|
-
# :reek:TooManyStatements: { max_statements: 6 }
|
98
|
+
# @return [Array<SmellWarning>] the smells found in the source
|
101
99
|
def run
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
$stderr.puts format(INCOMPREHENSIBLE_SOURCE_TEMPLATE, source.origin, exception.inspect)
|
109
|
-
else
|
110
|
-
smell_repository.report_on(collector)
|
111
|
-
end
|
112
|
-
self
|
100
|
+
return [] unless syntax_tree
|
101
|
+
begin
|
102
|
+
examine_tree
|
103
|
+
rescue StandardError => exception
|
104
|
+
$stderr.puts format(INCOMPREHENSIBLE_SOURCE_TEMPLATE, origin, exception.inspect)
|
105
|
+
[]
|
113
106
|
end
|
114
107
|
end
|
115
108
|
|
116
|
-
def
|
117
|
-
|
109
|
+
def syntax_tree
|
110
|
+
@syntax_tree ||= source.syntax_tree
|
111
|
+
end
|
112
|
+
|
113
|
+
def examine_tree
|
114
|
+
ContextBuilder.new(syntax_tree).context_tree.flat_map do |element|
|
118
115
|
smell_repository.examine(element)
|
119
116
|
end
|
120
117
|
end
|
@@ -52,7 +52,7 @@ module Reek
|
|
52
52
|
smell_warning(
|
53
53
|
context: ctx,
|
54
54
|
lines: found_call.lines,
|
55
|
-
message: "calls #{found_call.call} #{found_call.occurs} times",
|
55
|
+
message: "calls '#{found_call.call}' #{found_call.occurs} times",
|
56
56
|
parameters: { name: found_call.call, count: found_call.occurs })
|
57
57
|
end
|
58
58
|
end
|
@@ -48,7 +48,7 @@ module Reek
|
|
48
48
|
smell_warning(
|
49
49
|
context: ctx,
|
50
50
|
lines: lines,
|
51
|
-
message: "refers to #{name} more than self (maybe move it to another class?)",
|
51
|
+
message: "refers to '#{name}' more than self (maybe move it to another class?)",
|
52
52
|
parameters: { name: name.to_s })
|
53
53
|
end
|
54
54
|
end
|
@@ -13,9 +13,7 @@ module Reek
|
|
13
13
|
# - {file:README.md}
|
14
14
|
# for details.
|
15
15
|
#
|
16
|
-
# :reek:
|
17
|
-
# :reek:TooManyInstanceVariables: { max_instance_variables: 5 }
|
18
|
-
# :reek:UnusedPrivateMethod: { exclude: [ inherited, smell_warning ] }
|
16
|
+
# :reek:UnusedPrivateMethod: { exclude: [ smell_warning ] }
|
19
17
|
class SmellDetector
|
20
18
|
attr_reader :config
|
21
19
|
# The name of the config field that lists the names of code contexts
|
@@ -28,8 +26,7 @@ module Reek
|
|
28
26
|
DEFAULT_EXCLUDE_SET = [].freeze
|
29
27
|
|
30
28
|
def initialize(config = {})
|
31
|
-
@config
|
32
|
-
@smells_found = []
|
29
|
+
@config = SmellConfiguration.new self.class.default_config.merge(config)
|
33
30
|
end
|
34
31
|
|
35
32
|
def smell_type
|
@@ -41,14 +38,10 @@ module Reek
|
|
41
38
|
end
|
42
39
|
|
43
40
|
def run_for(context)
|
44
|
-
return unless enabled_for?(context)
|
45
|
-
return if exception?(context)
|
41
|
+
return [] unless enabled_for?(context)
|
42
|
+
return [] if exception?(context)
|
46
43
|
|
47
|
-
|
48
|
-
end
|
49
|
-
|
50
|
-
def report_on(collector)
|
51
|
-
smells_found.each { |smell| smell.report_on(collector) }
|
44
|
+
sniff(context)
|
52
45
|
end
|
53
46
|
|
54
47
|
def exception?(context)
|
@@ -63,8 +56,6 @@ module Reek
|
|
63
56
|
|
64
57
|
private
|
65
58
|
|
66
|
-
attr_accessor :smells_found
|
67
|
-
|
68
59
|
def enabled_for?(context)
|
69
60
|
config.enabled? && config_for(context)[SmellConfiguration::ENABLED_KEY] != false
|
70
61
|
end
|
@@ -38,12 +38,8 @@ module Reek
|
|
38
38
|
@detectors = smell_types.map { |klass| klass.new configuration_for(klass) }
|
39
39
|
end
|
40
40
|
|
41
|
-
def report_on(collector)
|
42
|
-
detectors.each { |detector| detector.report_on(collector) }
|
43
|
-
end
|
44
|
-
|
45
41
|
def examine(context)
|
46
|
-
smell_detectors_for(context.type).
|
42
|
+
smell_detectors_for(context.type).flat_map do |detector|
|
47
43
|
detector.run_for(context)
|
48
44
|
end
|
49
45
|
end
|
@@ -31,16 +31,18 @@ module Reek
|
|
31
31
|
@lines = lines
|
32
32
|
@message = message
|
33
33
|
@parameters = parameters
|
34
|
+
|
35
|
+
freeze
|
34
36
|
end
|
35
37
|
|
36
38
|
# @public
|
37
39
|
def hash
|
38
|
-
|
40
|
+
identifying_values.hash
|
39
41
|
end
|
40
42
|
|
41
43
|
# @public
|
42
44
|
def <=>(other)
|
43
|
-
|
45
|
+
identifying_values <=> other.identifying_values
|
44
46
|
end
|
45
47
|
|
46
48
|
# @public
|
@@ -48,10 +50,6 @@ module Reek
|
|
48
50
|
(self <=> other).zero?
|
49
51
|
end
|
50
52
|
|
51
|
-
def report_on(listener)
|
52
|
-
listener.found_smell(self)
|
53
|
-
end
|
54
|
-
|
55
53
|
# @public
|
56
54
|
def to_hash
|
57
55
|
stringified_params = Hash[parameters.map { |key, val| [key.to_s, val] }]
|
@@ -70,8 +68,8 @@ module Reek
|
|
70
68
|
|
71
69
|
protected
|
72
70
|
|
73
|
-
def
|
74
|
-
[smell_type, context, message]
|
71
|
+
def identifying_values
|
72
|
+
[smell_type, context, message, lines]
|
75
73
|
end
|
76
74
|
|
77
75
|
private
|
@@ -13,9 +13,11 @@ module Reek
|
|
13
13
|
# and they hurt the flow of reading, because the reader must slow
|
14
14
|
# down to interpret the names.
|
15
15
|
#
|
16
|
-
# Currently +UncommunicativeName+ checks for
|
17
|
-
#
|
18
|
-
# * names
|
16
|
+
# Currently +UncommunicativeName+ checks for:
|
17
|
+
#
|
18
|
+
# * single-character names
|
19
|
+
# * any name ending with a number
|
20
|
+
# * camelCaseVariableNames
|
19
21
|
#
|
20
22
|
# See {file:docs/Uncommunicative-Variable-Name.md} for details.
|
21
23
|
#
|
@@ -24,13 +26,17 @@ module Reek
|
|
24
26
|
# The name of the config field that lists the regexps of
|
25
27
|
# smelly names to be reported.
|
26
28
|
REJECT_KEY = 'reject'.freeze
|
27
|
-
DEFAULT_REJECT_SET = [
|
29
|
+
DEFAULT_REJECT_SET = [
|
30
|
+
/^.$/, # single-character names
|
31
|
+
/[0-9]$/, # any name ending with a number
|
32
|
+
/[A-Z]/ # camelCaseVariableNames
|
33
|
+
].freeze
|
28
34
|
|
29
35
|
# The name of the config field that lists the specific names that are
|
30
36
|
# to be treated as exceptions; these names will not be reported as
|
31
37
|
# uncommunicative.
|
32
38
|
ACCEPT_KEY = 'accept'.freeze
|
33
|
-
DEFAULT_ACCEPT_SET = [
|
39
|
+
DEFAULT_ACCEPT_SET = [/^_$/].freeze
|
34
40
|
|
35
41
|
def self.default_config
|
36
42
|
super.merge(
|
@@ -51,7 +57,7 @@ module Reek
|
|
51
57
|
self.reject_names = value(REJECT_KEY, ctx)
|
52
58
|
self.accept_names = value(ACCEPT_KEY, ctx)
|
53
59
|
variable_names(ctx.exp).select do |name, _lines|
|
54
|
-
|
60
|
+
uncommunicative_variable_name?(name)
|
55
61
|
end.map do |name, lines|
|
56
62
|
smell_warning(
|
57
63
|
context: ctx,
|
@@ -61,10 +67,16 @@ module Reek
|
|
61
67
|
end
|
62
68
|
end
|
63
69
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
70
|
+
private
|
71
|
+
|
72
|
+
def uncommunicative_variable_name?(name)
|
73
|
+
sanitized_name = name.to_s.gsub(/^[@\*\&]*/, '')
|
74
|
+
!acceptable_name?(sanitized_name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def acceptable_name?(name)
|
78
|
+
Array(accept_names).any? { |accept_pattern| name.match accept_pattern } ||
|
79
|
+
Array(reject_names).none? { |reject_pattern| name.match reject_pattern }
|
68
80
|
end
|
69
81
|
|
70
82
|
# :reek:TooManyStatements: { max_statements: 6 }
|
@@ -124,8 +136,6 @@ module Reek
|
|
124
136
|
accumulator[var].push(exp.line)
|
125
137
|
end
|
126
138
|
|
127
|
-
private
|
128
|
-
|
129
139
|
attr_accessor :accept_names, :reject_names
|
130
140
|
end
|
131
141
|
end
|
data/lib/reek/spec.rb
CHANGED
@@ -38,8 +38,8 @@ module Reek
|
|
38
38
|
# To check for specific smells, use something like this:
|
39
39
|
#
|
40
40
|
# ruby = 'def double_thing() @other.thing.foo + @other.thing.foo end'
|
41
|
-
# ruby.should reek_of(:
|
42
|
-
# ruby.should reek_of(:
|
41
|
+
# ruby.should reek_of(:DuplicateMethodCall, name: '@other.thing')
|
42
|
+
# ruby.should reek_of(:DuplicateMethodCall, name: '@other.thing.foo', count: 2)
|
43
43
|
# ruby.should_not reek_of(:FeatureEnvy)
|
44
44
|
#
|
45
45
|
# @public
|
@@ -9,6 +9,13 @@ module Reek
|
|
9
9
|
# code smell.
|
10
10
|
#
|
11
11
|
class ShouldReekOf
|
12
|
+
# Variant of Examiner that doesn't swallow exceptions
|
13
|
+
class UnsafeExaminer < Examiner
|
14
|
+
def run
|
15
|
+
examine_tree
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
12
19
|
attr_reader :failure_message, :failure_message_when_negated
|
13
20
|
|
14
21
|
def initialize(smell_type_or_class,
|
@@ -16,13 +23,15 @@ module Reek
|
|
16
23
|
configuration = Configuration::AppConfiguration.default)
|
17
24
|
@smell_type = normalize smell_type_or_class
|
18
25
|
@smell_details = smell_details
|
26
|
+
configuration.load_values(smell_type => { Smells::SmellConfiguration::ENABLED_KEY => true })
|
19
27
|
@configuration = configuration
|
20
|
-
@configuration.load_values(smell_type => { Smells::SmellConfiguration::ENABLED_KEY => true })
|
21
28
|
end
|
22
29
|
|
23
30
|
def matches?(source)
|
24
31
|
@matching_smell_types = nil
|
25
|
-
self.examiner =
|
32
|
+
self.examiner = UnsafeExaminer.new(source,
|
33
|
+
filter_by_smells: [smell_type],
|
34
|
+
configuration: configuration)
|
26
35
|
set_failure_messages
|
27
36
|
matching_smell_details?
|
28
37
|
end
|
@@ -49,12 +58,7 @@ module Reek
|
|
49
58
|
end
|
50
59
|
|
51
60
|
def matching_smell_types
|
52
|
-
@matching_smell_types ||=
|
53
|
-
select { |it| it.matches_smell_type?(smell_type) }
|
54
|
-
end
|
55
|
-
|
56
|
-
def smell_matchers
|
57
|
-
examiner.smells.map { |it| SmellMatcher.new(it) }
|
61
|
+
@matching_smell_types ||= examiner.smells.map { |it| SmellMatcher.new(it) }
|
58
62
|
end
|
59
63
|
|
60
64
|
def matching_smell_types?
|