reek 5.4.1 → 6.0.2

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +8 -6
  4. data/.rubocop_todo.yml +25 -20
  5. data/.simplecov +1 -0
  6. data/.travis.yml +17 -11
  7. data/CHANGELOG.md +31 -3
  8. data/Dockerfile +1 -0
  9. data/Gemfile +14 -17
  10. data/README.md +15 -11
  11. data/bin/code_climate_reek +12 -2
  12. data/docs/Attribute.md +1 -1
  13. data/docs/Boolean-Parameter.md +1 -1
  14. data/docs/Control-Couple.md +1 -1
  15. data/docs/Nil-Check.md +4 -1
  16. data/features/command_line_interface/options.feature +2 -3
  17. data/features/configuration_files/schema_validation.feature +1 -1
  18. data/features/reports/codeclimate.feature +2 -2
  19. data/features/reports/json.feature +3 -3
  20. data/features/reports/reports.feature +4 -4
  21. data/features/reports/yaml.feature +3 -3
  22. data/features/step_definitions/reek_steps.rb +5 -1
  23. data/features/step_definitions/sample_file_steps.rb +2 -2
  24. data/features/support/env.rb +1 -2
  25. data/lib/reek.rb +1 -0
  26. data/lib/reek/ast/sexp_extensions/arguments.rb +11 -0
  27. data/lib/reek/cli/options.rb +3 -3
  28. data/lib/reek/code_comment.rb +45 -38
  29. data/lib/reek/configuration/app_configuration.rb +4 -3
  30. data/lib/reek/configuration/configuration_converter.rb +2 -2
  31. data/lib/reek/configuration/directory_directives.rb +9 -3
  32. data/lib/reek/context/module_context.rb +3 -1
  33. data/lib/reek/errors/legacy_comment_separator_error.rb +36 -0
  34. data/lib/reek/examiner.rb +3 -3
  35. data/lib/reek/report.rb +5 -7
  36. data/lib/reek/report/code_climate/code_climate_configuration.yml +1 -1
  37. data/lib/reek/report/code_climate/code_climate_report.rb +2 -1
  38. data/lib/reek/report/simple_warning_formatter.rb +0 -7
  39. data/lib/reek/report/text_report.rb +2 -2
  40. data/lib/reek/smell_detectors/base_detector.rb +1 -9
  41. data/lib/reek/smell_detectors/boolean_parameter.rb +3 -1
  42. data/lib/reek/smell_detectors/data_clump.rb +23 -56
  43. data/lib/reek/smell_detectors/nil_check.rb +1 -12
  44. data/lib/reek/smell_detectors/subclassed_from_core_class.rb +3 -7
  45. data/lib/reek/smell_detectors/uncommunicative_variable_name.rb +1 -1
  46. data/lib/reek/smell_warning.rb +1 -2
  47. data/lib/reek/source/source_code.rb +3 -2
  48. data/lib/reek/source/source_locator.rb +13 -10
  49. data/lib/reek/spec/smell_matcher.rb +2 -1
  50. data/lib/reek/version.rb +1 -1
  51. data/reek.gemspec +13 -6
  52. data/spec/performance/reek/smell_detectors/runtime_speed_spec.rb +2 -4
  53. data/spec/quality/documentation_spec.rb +2 -1
  54. data/spec/reek/ast/sexp_extensions_spec.rb +15 -33
  55. data/spec/reek/code_comment_spec.rb +41 -42
  56. data/spec/reek/configuration/directory_directives_spec.rb +6 -0
  57. data/spec/reek/context_builder_spec.rb +110 -113
  58. data/spec/reek/report/code_climate/code_climate_configuration_spec.rb +1 -3
  59. data/spec/reek/report/code_climate/code_climate_fingerprint_spec.rb +26 -26
  60. data/spec/reek/report/code_climate/code_climate_formatter_spec.rb +6 -6
  61. data/spec/reek/report/code_climate/code_climate_report_spec.rb +1 -1
  62. data/spec/reek/report/json_report_spec.rb +1 -1
  63. data/spec/reek/report/location_formatter_spec.rb +3 -3
  64. data/spec/reek/report/text_report_spec.rb +1 -7
  65. data/spec/reek/report/yaml_report_spec.rb +1 -1
  66. data/spec/reek/smell_detectors/base_detector_spec.rb +3 -13
  67. data/spec/reek/smell_detectors/data_clump_spec.rb +14 -0
  68. data/spec/reek/smell_detectors/missing_safe_method_spec.rb +8 -2
  69. data/spec/reek/smell_detectors/nil_check_spec.rb +3 -3
  70. data/spec/reek/smell_warning_spec.rb +12 -12
  71. data/spec/reek/source/source_code_spec.rb +13 -0
  72. data/spec/reek/spec/should_reek_of_spec.rb +0 -1
  73. data/spec/reek/spec/should_reek_only_of_spec.rb +6 -6
  74. data/spec/reek/spec/smell_matcher_spec.rb +1 -1
  75. data/spec/spec_helper.rb +20 -6
  76. data/tasks/configuration.rake +1 -2
  77. metadata +15 -25
  78. data/spec/factories/factories.rb +0 -37
@@ -105,11 +105,11 @@ module Reek
105
105
  rescue Errors::BaseError
106
106
  raise
107
107
  rescue EncodingError
108
- raise Errors::EncodingError, origin: origin
108
+ raise Errors::EncodingError.new(origin: origin)
109
109
  rescue Parser::SyntaxError
110
- raise Errors::SyntaxError, origin: origin
110
+ raise Errors::SyntaxError.new(origin: origin)
111
111
  rescue StandardError
112
- raise Errors::IncomprehensibleSourceError, origin: origin
112
+ raise Errors::IncomprehensibleSourceError.new(origin: origin)
113
113
  end
114
114
 
115
115
  def syntax_tree
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'report/code_climate'
4
3
  require_relative 'report/html_report'
5
4
  require_relative 'report/json_report'
6
5
  require_relative 'report/text_report'
@@ -17,12 +16,11 @@ module Reek
17
16
  # Reek reporting functionality.
18
17
  module Report
19
18
  REPORT_CLASSES = {
20
- yaml: YAMLReport,
21
- json: JSONReport,
22
- html: HTMLReport,
23
- xml: XMLReport,
24
- text: TextReport,
25
- code_climate: CodeClimateReport
19
+ yaml: YAMLReport,
20
+ json: JSONReport,
21
+ html: HTMLReport,
22
+ xml: XMLReport,
23
+ text: TextReport
26
24
  }.freeze
27
25
 
28
26
  LOCATION_FORMATTERS = {
@@ -59,7 +59,7 @@ BooleanParameter:
59
59
 
60
60
  ## Getting rid of the smell
61
61
 
62
- This is highly dependant on your exact architecture, but looking at the example above what you could do is:
62
+ This is highly dependent on your exact architecture, but looking at the example above what you could do is:
63
63
 
64
64
  * Move everything in the `if` branch into a separate method
65
65
  * Move everything in the `else` branch into a separate method
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../base_report'
4
+ require_relative 'code_climate_formatter'
4
5
 
5
6
  module Reek
6
7
  module Report
@@ -12,7 +13,7 @@ module Reek
12
13
  class CodeClimateReport < BaseReport
13
14
  def show(out = $stdout)
14
15
  smells.map do |smell|
15
- out.print warning_formatter.format_code_climate_hash(smell)
16
+ out.print CodeClimateFormatter.new(smell).render
16
17
  end
17
18
  end
18
19
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'code_climate/code_climate_formatter'
4
-
5
3
  module Reek
6
4
  module Report
7
5
  #
@@ -17,11 +15,6 @@ module Reek
17
15
  "#{location_formatter.format(warning)}#{warning.base_message}"
18
16
  end
19
17
 
20
- # @quality :reek:UtilityFunction
21
- def format_code_climate_hash(warning)
22
- CodeClimateFormatter.new(warning).render
23
- end
24
-
25
18
  def format_list(warnings)
26
19
  warnings.map { |warning| " #{format(warning)}" }.join("\n")
27
20
  end
@@ -11,8 +11,8 @@ module Reek
11
11
  #
12
12
  class TextReport < BaseReport
13
13
  # @public
14
- def initialize(*args)
15
- super(*args)
14
+ def initialize(**args)
15
+ super
16
16
 
17
17
  print progress_formatter.header
18
18
  end
@@ -19,6 +19,7 @@ module Reek
19
19
  # @quality :reek:TooManyMethods { max_methods: 18 }
20
20
  class BaseDetector
21
21
  attr_reader :config
22
+
22
23
  # The name of the config field that lists the names of code contexts
23
24
  # that should not be checked. Add this field to the config for each
24
25
  # smell that should ignore this code element.
@@ -121,15 +122,6 @@ module Reek
121
122
  @descendants ||= []
122
123
  end
123
124
 
124
- #
125
- # @param detector [String] the detector in question, e.g. 'DuplicateMethodCall'
126
- # @return [Boolean]
127
- #
128
- def valid_detector?(detector)
129
- descendants.map { |descendant| descendant.to_s.split('::').last }.
130
- include?(detector)
131
- end
132
-
133
125
  #
134
126
  # Transform a detector name to the corresponding constant.
135
127
  # Note that we assume a valid name - exceptions are not handled here.
@@ -14,6 +14,8 @@ module Reek
14
14
  #
15
15
  # See {file:docs/Boolean-Parameter.md} for details.
16
16
  class BooleanParameter < BaseDetector
17
+ BOOLEAN_VALUES = [:true, :false].freeze
18
+
17
19
  #
18
20
  # Checks whether the given method has any Boolean parameters.
19
21
  #
@@ -21,7 +23,7 @@ module Reek
21
23
  #
22
24
  def sniff
23
25
  context.default_assignments.select do |_parameter, value|
24
- [:true, :false].include?(value.type)
26
+ BOOLEAN_VALUES.include?(value.type)
25
27
  end.map do |parameter, _value|
26
28
  smell_warning(
27
29
  lines: [source_line],
@@ -51,7 +51,7 @@ module Reek
51
51
  # @return [Array<SmellWarning>]
52
52
  #
53
53
  def sniff
54
- MethodGroup.new(context, min_clump_size, max_copies).clumps.map do |clump, methods|
54
+ clumps.map do |clump, methods|
55
55
  methods_length = methods.length
56
56
  smell_warning(
57
57
  lines: methods.map(&:line),
@@ -72,72 +72,39 @@ module Reek
72
72
  private
73
73
 
74
74
  def max_copies
75
- value(MAX_COPIES_KEY, context)
75
+ @max_copies ||= value(MAX_COPIES_KEY, context)
76
76
  end
77
77
 
78
78
  def min_clump_size
79
- value(MIN_CLUMP_SIZE_KEY, context)
79
+ @min_clump_size ||= value(MIN_CLUMP_SIZE_KEY, context)
80
80
  end
81
- end
82
- end
83
81
 
84
- # Represents a group of methods
85
- # @private
86
- class MethodGroup
87
- def initialize(ctx, min_clump_size, max_copies)
88
- @min_clump_size = min_clump_size
89
- @max_copies = max_copies
90
- @candidate_methods = ctx.node_instance_methods.map do |defn_node|
91
- CandidateMethod.new(defn_node)
82
+ def candidate_methods
83
+ @candidate_methods ||= context.node_instance_methods
92
84
  end
93
- end
94
85
 
95
- def candidate_clumps
96
- candidate_methods.each_cons(max_copies + 1).map do |methods|
97
- common_argument_names_for(methods)
98
- end.select do |clump|
99
- clump.length >= min_clump_size
100
- end.uniq
101
- end
102
-
103
- # @quality :reek:UtilityFunction
104
- def common_argument_names_for(methods)
105
- methods.map(&:arg_names).inject(:&)
106
- end
107
-
108
- def methods_containing_clump(clump)
109
- candidate_methods.select { |method| clump & method.arg_names == clump }
110
- end
111
-
112
- def clumps
113
- candidate_clumps.map do |clump|
114
- [clump, methods_containing_clump(clump)]
86
+ def candidate_clumps
87
+ candidate_methods.each_cons(max_copies + 1).map do |methods|
88
+ common_argument_names_for(methods)
89
+ end.select do |clump|
90
+ clump.length >= min_clump_size
91
+ end.uniq
115
92
  end
116
- end
117
-
118
- private
119
93
 
120
- attr_reader :candidate_methods, :max_copies, :min_clump_size
121
- end
122
-
123
- # A method definition and a copy of its parameters
124
- # @private
125
- class CandidateMethod
126
- extend Forwardable
127
-
128
- def_delegators :defn, :line, :name
94
+ # @quality :reek:UtilityFunction
95
+ def common_argument_names_for(methods)
96
+ methods.map(&:arg_names).inject(:&).compact.sort
97
+ end
129
98
 
130
- def initialize(defn_node)
131
- @defn = defn_node
132
- end
99
+ def methods_containing_clump(clump)
100
+ candidate_methods.select { |method| clump & method.arg_names == clump }
101
+ end
133
102
 
134
- def arg_names
135
- # TODO: Is all this sorting still needed?
136
- @arg_names ||= defn.arg_names.compact.sort
103
+ def clumps
104
+ candidate_clumps.map do |clump|
105
+ [clump, methods_containing_clump(clump)]
106
+ end
107
+ end
137
108
  end
138
-
139
- private
140
-
141
- attr_reader :defn
142
109
  end
143
110
  end
@@ -24,8 +24,7 @@ module Reek
24
24
 
25
25
  def detect_nodes
26
26
  finders = [NodeFinder.new(context, :send, NilCallNodeDetector),
27
- NodeFinder.new(context, :when, NilWhenNodeDetector),
28
- NodeFinder.new(context, :csend, SafeNavigationNodeDetector)]
27
+ NodeFinder.new(context, :when, NilWhenNodeDetector)]
29
28
  finders.flat_map(&:smelly_nodes)
30
29
  end
31
30
 
@@ -88,16 +87,6 @@ module Reek
88
87
  node.condition_list.any? { |it| it.type == :nil }
89
88
  end
90
89
  end
91
-
92
- # Detect safe navigation. Returns true for all nodes, since all :csend
93
- # nodes are considered smelly.
94
- module SafeNavigationNodeDetector
95
- module_function
96
-
97
- def detect(_node)
98
- true
99
- end
100
- end
101
90
  end
102
91
  end
103
92
  end
@@ -43,13 +43,9 @@ module Reek
43
43
  end
44
44
 
45
45
  def build_smell_warning(ancestor_name)
46
- smell_attributes = {
47
- lines: [source_line],
48
- message: "inherits from core class '#{ancestor_name}'",
49
- parameters: { ancestor: ancestor_name }
50
- }
51
-
52
- smell_warning(smell_attributes)
46
+ smell_warning(lines: [source_line],
47
+ message: "inherits from core class '#{ancestor_name}'",
48
+ parameters: { ancestor: ancestor_name })
53
49
  end
54
50
  end
55
51
  end
@@ -74,7 +74,7 @@ module Reek
74
74
  end
75
75
 
76
76
  def uncommunicative_variable_name?(name)
77
- sanitized_name = name.to_s.gsub(/^[@\*\&]*/, '')
77
+ sanitized_name = name.to_s.gsub(/^[@*&]*/, '')
78
78
  !acceptable_name?(sanitized_name)
79
79
  end
80
80
 
@@ -31,8 +31,7 @@ module Reek
31
31
  # public API.
32
32
  #
33
33
  # @quality :reek:LongParameterList { max_params: 6 }
34
- def initialize(smell_type, context: '', lines:, message:,
35
- source:, parameters: {})
34
+ def initialize(smell_type, lines:, message:, source:, context: '', parameters: {})
36
35
  @smell_type = smell_type
37
36
  @source = source
38
37
  @context = context.to_s
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../cli/silencer'
4
- Reek::CLI::Silencer.without_warnings { require 'parser/ruby26' }
4
+ # Silence Parser's warnings about Ruby micro version differences
5
+ Reek::CLI::Silencer.silently { require 'parser/current' }
5
6
  require_relative '../tree_dresser'
6
7
  require_relative '../ast/node'
7
8
  require_relative '../ast/builder'
@@ -53,7 +54,7 @@ module Reek
53
54
  end
54
55
 
55
56
  def self.default_parser
56
- Parser::Ruby26.new(AST::Builder.new).tap do |parser|
57
+ Parser::CurrentRuby.new(AST::Builder.new).tap do |parser|
57
58
  diagnostics = parser.diagnostics
58
59
  diagnostics.all_errors_are_fatal = true
59
60
  diagnostics.ignore_warnings = true
@@ -33,23 +33,26 @@ module Reek
33
33
 
34
34
  attr_reader :configuration, :paths, :options
35
35
 
36
- # @quality :reek:TooManyStatements { max_statements: 7 }
37
- # @quality :reek:NestedIterators { max_allowed_nesting: 2 }
38
36
  def source_paths
39
37
  paths.each_with_object([]) do |given_path, relevant_paths|
40
- unless given_path.exist?
38
+ if given_path.exist?
39
+ relevant_paths.concat source_files_from_path(given_path)
40
+ else
41
41
  print_no_such_file_error(given_path)
42
- next
43
42
  end
43
+ end
44
+ end
44
45
 
45
- given_path.find do |path|
46
- if path.directory?
47
- ignore_path?(path) ? Find.prune : next
48
- elsif ruby_file?(path)
49
- relevant_paths << path unless ignore_file?(path)
50
- end
46
+ def source_files_from_path(given_path)
47
+ relevant_paths = []
48
+ given_path.find do |path|
49
+ if path.directory?
50
+ Find.prune if ignore_path?(path)
51
+ elsif ruby_file?(path)
52
+ relevant_paths << path unless ignore_file?(path)
51
53
  end
52
54
  end
55
+ relevant_paths
53
56
  end
54
57
 
55
58
  def ignore_file?(path)
@@ -43,8 +43,9 @@ module Reek
43
43
  raise ArgumentError, "The attribute '#{extra_keys.first}' is not available for comparison"
44
44
  end
45
45
 
46
+ # :reek:FeatureEnvy
46
47
  def common_parameters_equal?(other_parameters)
47
- smell_warning.parameters.slice(*other_parameters.keys) == other_parameters
48
+ smell_warning.parameters.values_at(*other_parameters.keys) == other_parameters.values
48
49
  end
49
50
 
50
51
  def common_attributes_equal?(attributes)
@@ -8,6 +8,6 @@ module Reek
8
8
  # @public
9
9
  module Version
10
10
  # @public
11
- STRING = '5.4.1'
11
+ STRING = '6.0.2'
12
12
  end
13
13
  end
@@ -16,12 +16,19 @@ Gem::Specification.new do |s|
16
16
  s.executables = s.files.grep(%r{^bin/}).map { |path| File.basename(path) }
17
17
  s.homepage = 'https://github.com/troessner/reek'
18
18
  s.rdoc_options = %w(--main README.md -x assets/|bin/|config/|features/|spec/|tasks/)
19
- s.required_ruby_version = '>= 2.3.0'
19
+ s.required_ruby_version = '>= 2.4.0'
20
20
  s.summary = 'Code smell detector for Ruby'
21
21
 
22
- s.add_runtime_dependency 'codeclimate-engine-rb', '~> 0.4.0'
23
- s.add_runtime_dependency 'kwalify', '~> 0.7.0'
24
- s.add_runtime_dependency 'parser', '< 2.7', '>= 2.5.0.0', '!= 2.5.1.1'
25
- s.add_runtime_dependency 'psych', '~> 3.1.0'
26
- s.add_runtime_dependency 'rainbow', '>= 2.0', '< 4.0'
22
+ s.metadata = {
23
+ 'homepage_uri' => 'https://github.com/troessner/reek',
24
+ 'source_code_uri' => 'https://github.com/troessner/reek',
25
+ 'bug_tracker_uri' => 'https://github.com/troessner/reek/issues',
26
+ 'changelog_uri' => 'https://github.com/troessner/reek/CHANGELOG.md',
27
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/reek'
28
+ }
29
+
30
+ s.add_runtime_dependency 'kwalify', '~> 0.7.0'
31
+ s.add_runtime_dependency 'parser', '< 2.8', '>= 2.5.0.0', '!= 2.5.1.1'
32
+ s.add_runtime_dependency 'psych', '~> 3.1'
33
+ s.add_runtime_dependency 'rainbow', '>= 2.0', '< 4.0'
27
34
  end
@@ -6,10 +6,8 @@ RSpec.describe 'Runtime speed' do
6
6
 
7
7
  it 'runs on our smelly sources in less than 5 seconds' do
8
8
  expect do
9
- source_directory.each_entry do |entry|
10
- next if %w(. ..).include?(entry.to_s)
11
-
12
- examiner = Reek::Examiner.new entry.to_path
9
+ Dir[source_directory.join('**/*.rb')].each do |entry|
10
+ examiner = Reek::Examiner.new Pathname.new(entry)
13
11
  examiner.smells.size
14
12
  end
15
13
  end.to perform_under(5).sec
@@ -4,6 +4,7 @@ require 'kramdown'
4
4
  RSpec.describe 'Documentation' do
5
5
  root = File.expand_path('../..', __dir__)
6
6
  files = Dir.glob(File.join(root, '*.md')) + Dir.glob(File.join(root, 'docs', '*.md'))
7
+ code_types = [:codeblock, :codespan]
7
8
 
8
9
  files.each do |file|
9
10
  describe "from #{file}" do
@@ -13,7 +14,7 @@ RSpec.describe 'Documentation' do
13
14
 
14
15
  blocks.each do |block|
15
16
  # Only consider code blocks with language 'ruby'.
16
- next unless [:codeblock, :codespan].include?(block.type)
17
+ next unless code_types.include?(block.type)
17
18
  next unless block.attr['class'] == 'language-ruby'
18
19
 
19
20
  it "has a valid sample at #{block.options[:location] + 1}" do