reek 4.4.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/CONTRIBUTING.md +41 -4
  4. data/README.md +15 -3
  5. data/defaults.reek +1 -1
  6. data/docs/Basic-Smell-Options.md +2 -2
  7. data/docs/Code-Smells.md +4 -0
  8. data/docs/How-To-Write-New-Detectors.md +116 -0
  9. data/docs/How-reek-works-internally.md +3 -4
  10. data/docs/Instance-Variable-Assumption.md +134 -0
  11. data/docs/Simulated-Polymorphism.md +1 -1
  12. data/docs/Smell-Suppression.md +6 -6
  13. data/docs/{style-guide.md → Style-Guide.md} +0 -0
  14. data/docs/Unused-Private-Method.md +1 -1
  15. data/docs/YAML-Reports.md +0 -18
  16. data/features/configuration_files/directory_specific_directives.feature +4 -4
  17. data/features/configuration_files/unused_private_method.feature +2 -2
  18. data/features/samples.feature +122 -117
  19. data/features/smells/subclassed_from_core_class.feature +1 -1
  20. data/lib/reek/code_comment.rb +13 -4
  21. data/lib/reek/context/code_context.rb +1 -0
  22. data/lib/reek/examiner.rb +24 -27
  23. data/lib/reek/smells/class_variable.rb +1 -1
  24. data/lib/reek/smells/control_parameter.rb +1 -1
  25. data/lib/reek/smells/data_clump.rb +1 -1
  26. data/lib/reek/smells/duplicate_method_call.rb +1 -1
  27. data/lib/reek/smells/feature_envy.rb +1 -1
  28. data/lib/reek/smells/instance_variable_assumption.rb +1 -1
  29. data/lib/reek/smells/prima_donna_method.rb +1 -1
  30. data/lib/reek/smells/repeated_conditional.rb +1 -1
  31. data/lib/reek/smells/smell_detector.rb +5 -14
  32. data/lib/reek/smells/smell_repository.rb +1 -5
  33. data/lib/reek/smells/smell_warning.rb +6 -8
  34. data/lib/reek/smells/subclassed_from_core_class.rb +1 -1
  35. data/lib/reek/smells/uncommunicative_variable_name.rb +22 -12
  36. data/lib/reek/smells/unused_private_method.rb +1 -1
  37. data/lib/reek/spec.rb +2 -2
  38. data/lib/reek/spec/should_reek_of.rb +12 -8
  39. data/lib/reek/version.rb +1 -1
  40. data/spec/reek/code_comment_spec.rb +13 -5
  41. data/spec/reek/examiner_spec.rb +2 -2
  42. data/spec/reek/smells/attribute_spec.rb +91 -78
  43. data/spec/reek/smells/boolean_parameter_spec.rb +72 -64
  44. data/spec/reek/smells/class_variable_spec.rb +81 -68
  45. data/spec/reek/smells/control_parameter_spec.rb +101 -141
  46. data/spec/reek/smells/data_clump_spec.rb +94 -149
  47. data/spec/reek/smells/duplicate_method_call_spec.rb +98 -85
  48. data/spec/reek/smells/feature_envy_spec.rb +164 -183
  49. data/spec/reek/smells/instance_variable_assumption_spec.rb +51 -147
  50. data/spec/reek/smells/irresponsible_module_spec.rb +153 -170
  51. data/spec/reek/smells/long_parameter_list_spec.rb +44 -88
  52. data/spec/reek/smells/long_yield_list_spec.rb +41 -41
  53. data/spec/reek/smells/manual_dispatch_spec.rb +36 -18
  54. data/spec/reek/smells/module_initialize_spec.rb +31 -33
  55. data/spec/reek/smells/nested_iterators_spec.rb +189 -183
  56. data/spec/reek/smells/nil_check_spec.rb +48 -37
  57. data/spec/reek/smells/prima_donna_method_spec.rb +41 -26
  58. data/spec/reek/smells/repeated_conditional_spec.rb +75 -87
  59. data/spec/reek/smells/smell_warning_spec.rb +7 -0
  60. data/spec/reek/smells/subclassed_from_core_class_spec.rb +37 -112
  61. data/spec/reek/smells/too_many_constants_spec.rb +109 -199
  62. data/spec/reek/smells/too_many_instance_variables_spec.rb +105 -128
  63. data/spec/reek/smells/too_many_methods_spec.rb +38 -62
  64. data/spec/reek/smells/too_many_statements_spec.rb +69 -45
  65. data/spec/reek/smells/uncommunicative_method_name_spec.rb +16 -29
  66. data/spec/reek/smells/uncommunicative_module_name_spec.rb +24 -37
  67. data/spec/reek/smells/uncommunicative_parameter_name_spec.rb +55 -60
  68. data/spec/reek/smells/uncommunicative_variable_name_spec.rb +108 -95
  69. data/spec/reek/smells/unused_parameters_spec.rb +73 -49
  70. data/spec/reek/smells/unused_private_method_spec.rb +97 -50
  71. data/spec/reek/smells/utility_function_spec.rb +130 -188
  72. data/spec/reek/spec/should_reek_of_spec.rb +2 -2
  73. metadata +6 -7
  74. data/lib/reek/cli/warning_collector.rb +0 -27
  75. data/spec/reek/cli/warning_collector_spec.rb +0 -25
  76. data/spec/reek/smells/smell_detector_shared.rb +0 -29
@@ -16,6 +16,7 @@ module Reek
16
16
  # :reek:TooManyMethods: { max_methods: 19 }
17
17
  # :reek:TooManyInstanceVariables: { max_instance_variables: 8 }
18
18
  class CodeContext
19
+ include Enumerable
19
20
  extend Forwardable
20
21
  delegate each_node: :exp
21
22
  delegate %i(name type) => :exp
@@ -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
- # FIXME: Should be named "origin"
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
- @description ||= source.origin
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 :collector, :source, :smell_repository
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 an instance of Examiner
99
- #
100
- # :reek:TooManyStatements: { max_statements: 6 }
98
+ # @return [Array<SmellWarning>] the smells found in the source
101
99
  def run
102
- @run ||= begin
103
- syntax_tree = source.syntax_tree
104
- return self unless syntax_tree
105
- begin
106
- examine syntax_tree
107
- rescue StandardError => exception
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 examine(syntax_tree)
117
- ContextBuilder.new(syntax_tree).context_tree.each do |element|
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
@@ -29,7 +29,7 @@ module Reek
29
29
  smell_warning(
30
30
  context: ctx,
31
31
  lines: lines,
32
- message: "declares the class variable #{variable}",
32
+ message: "declares the class variable '#{variable}'",
33
33
  parameters: { name: variable.to_s })
34
34
  end
35
35
  end
@@ -56,7 +56,7 @@ module Reek
56
56
  smell_warning(
57
57
  context: ctx,
58
58
  lines: control_parameter.lines,
59
- message: "is controlled by argument #{argument}",
59
+ message: "is controlled by argument '#{argument}'",
60
60
  parameters: { argument: argument })
61
61
  end
62
62
  end
@@ -70,7 +70,7 @@ module Reek
70
70
 
71
71
  # @private
72
72
  def self.print_clump(clump)
73
- "[#{clump.map(&:to_s).join(', ')}]"
73
+ "[#{clump.map { |parameter| "'#{parameter}'" }.join(', ')}]"
74
74
  end
75
75
  end
76
76
  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
@@ -33,7 +33,7 @@ module Reek
33
33
  private
34
34
 
35
35
  def build_smell_warning(ctx, assumption)
36
- message = "assumes too much for instance variable #{assumption}"
36
+ message = "assumes too much for instance variable '#{assumption}'"
37
37
 
38
38
  smell_warning(
39
39
  context: ctx,
@@ -52,7 +52,7 @@ module Reek
52
52
  smell_warning(
53
53
  context: ctx,
54
54
  lines: [ctx.exp.line],
55
- message: "has prima donna method `#{name}`",
55
+ message: "has prima donna method '#{name}'",
56
56
  parameters: { name: name })
57
57
  end
58
58
  end
@@ -58,7 +58,7 @@ module Reek
58
58
  smell_warning(
59
59
  context: ctx,
60
60
  lines: lines,
61
- message: "tests #{expression} at least #{occurs} times",
61
+ message: "tests '#{expression}' at least #{occurs} times",
62
62
  parameters: { name: expression, count: occurs })
63
63
  end
64
64
  end
@@ -13,9 +13,7 @@ module Reek
13
13
  # - {file:README.md}
14
14
  # for details.
15
15
  #
16
- # :reek:TooManyMethods: { max_methods: 19 }
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 = SmellConfiguration.new self.class.default_config.merge(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
- self.smells_found = smells_found + sniff(context)
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).each do |detector|
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
- sort_key.hash
40
+ identifying_values.hash
39
41
  end
40
42
 
41
43
  # @public
42
44
  def <=>(other)
43
- sort_key <=> other.sort_key
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 sort_key
74
- [smell_type, context, message]
71
+ def identifying_values
72
+ [smell_type, context, message, lines]
75
73
  end
76
74
 
77
75
  private
@@ -46,7 +46,7 @@ module Reek
46
46
  smell_attributes = {
47
47
  context: ctx,
48
48
  lines: [ctx.exp.line],
49
- message: "inherits from a core class (#{ancestor_name})",
49
+ message: "inherits from core class '#{ancestor_name}'",
50
50
  parameters: { ancestor: ancestor_name }
51
51
  }
52
52
 
@@ -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
- # * 1-character names
18
- # * names ending with a number
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 = [/^.$/, /[0-9]$/, /[A-Z]/].freeze
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 = ['_'].freeze
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
- bad_name?(name, ctx)
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
- def bad_name?(name, _ctx)
65
- var = name.to_s.gsub(/^[@\*\&]*/, '')
66
- return false if accept_names.include?(var)
67
- reject_names.find { |patt| patt =~ var }
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
@@ -43,7 +43,7 @@ module Reek
43
43
  smell_warning(
44
44
  context: ctx,
45
45
  lines: [hit.line],
46
- message: "has the unused private instance method `#{name}`",
46
+ message: "has the unused private instance method '#{name}'",
47
47
  parameters: { name: name })
48
48
  end
49
49
  end
@@ -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(:Duplication, /@other.thing[^\.]/)
42
- # ruby.should reek_of(:Duplication, /@other.thing.foo/)
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 = Examiner.new(source, configuration: configuration)
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 ||= smell_matchers.
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?
@@ -7,6 +7,6 @@ module Reek
7
7
  # @public
8
8
  module Version
9
9
  # @public
10
- STRING = '4.4.0'.freeze
10
+ STRING = '4.4.1'.freeze
11
11
  end
12
12
  end