reek 3.0.4 → 3.1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +6 -0
  3. data/README.md +41 -20
  4. data/features/configuration_files/directory_specific_directives.feature +280 -0
  5. data/features/configuration_files/masking_smells.feature +0 -14
  6. data/features/step_definitions/sample_file_steps.rb +2 -2
  7. data/lib/reek/ast/sexp_extensions.rb +72 -60
  8. data/lib/reek/cli/application.rb +2 -5
  9. data/lib/reek/cli/reek_command.rb +1 -1
  10. data/lib/reek/configuration/app_configuration.rb +115 -61
  11. data/lib/reek/configuration/configuration_file_finder.rb +19 -0
  12. data/lib/reek/context/code_context.rb +102 -29
  13. data/lib/reek/context/method_context.rb +11 -48
  14. data/lib/reek/context/module_context.rb +2 -6
  15. data/lib/reek/context/root_context.rb +7 -14
  16. data/lib/reek/examiner.rb +15 -10
  17. data/lib/reek/report/report.rb +5 -4
  18. data/lib/reek/smells/boolean_parameter.rb +1 -1
  19. data/lib/reek/smells/duplicate_method_call.rb +1 -5
  20. data/lib/reek/smells/irresponsible_module.rb +2 -2
  21. data/lib/reek/smells/smell_repository.rb +30 -16
  22. data/lib/reek/smells/utility_function.rb +1 -1
  23. data/lib/reek/source/source_code.rb +10 -6
  24. data/lib/reek/source/source_locator.rb +27 -26
  25. data/lib/reek/spec.rb +8 -6
  26. data/lib/reek/spec/should_reek.rb +6 -2
  27. data/lib/reek/spec/should_reek_of.rb +5 -2
  28. data/lib/reek/spec/should_reek_only_of.rb +1 -1
  29. data/lib/reek/tree_walker.rb +49 -21
  30. data/lib/reek/version.rb +1 -1
  31. data/spec/reek/ast/sexp_extensions_spec.rb +4 -12
  32. data/spec/reek/configuration/app_configuration_spec.rb +80 -52
  33. data/spec/reek/configuration/configuration_file_finder_spec.rb +27 -15
  34. data/spec/reek/context/code_context_spec.rb +66 -17
  35. data/spec/reek/context/method_context_spec.rb +66 -64
  36. data/spec/reek/context/root_context_spec.rb +3 -1
  37. data/spec/reek/context/singleton_method_context_spec.rb +1 -2
  38. data/spec/reek/examiner_spec.rb +5 -8
  39. data/spec/reek/report/xml_report_spec.rb +3 -2
  40. data/spec/reek/smells/attribute_spec.rb +12 -17
  41. data/spec/reek/smells/duplicate_method_call_spec.rb +17 -25
  42. data/spec/reek/smells/feature_envy_spec.rb +4 -5
  43. data/spec/reek/smells/irresponsible_module_spec.rb +43 -0
  44. data/spec/reek/smells/nested_iterators_spec.rb +12 -26
  45. data/spec/reek/smells/too_many_statements_spec.rb +2 -210
  46. data/spec/reek/smells/utility_function_spec.rb +49 -5
  47. data/spec/reek/source/source_locator_spec.rb +39 -43
  48. data/spec/reek/spec/should_reek_of_spec.rb +3 -2
  49. data/spec/reek/spec/should_reek_spec.rb +25 -17
  50. data/spec/reek/tree_walker_spec.rb +214 -23
  51. data/spec/samples/configuration/full_configuration.reek +9 -0
  52. data/spec/spec_helper.rb +10 -10
  53. metadata +4 -3
  54. data/features/configuration_files/overrides_defaults.feature +0 -15
@@ -1,63 +1,17 @@
1
1
  require_relative 'code_context'
2
- require_relative '../ast/object_refs'
3
2
 
4
3
  module Reek
5
4
  module Context
6
- #
7
- # The parameters in a method's definition.
8
- #
9
- # @api private
10
- module MethodParameters
11
- def default_assignments
12
- result = []
13
- each do |exp|
14
- result << exp[1..2] if exp.optional_argument?
15
- end
16
- result
17
- end
18
- end
19
-
20
5
  #
21
6
  # A context wrapper for any method definition found in a syntax tree.
22
7
  #
23
8
  # @api private
24
9
  class MethodContext < CodeContext
25
- attr_reader :parameters
26
10
  attr_reader :refs
27
- attr_reader :num_statements
28
-
29
- def initialize(outer, exp)
30
- super
31
- @parameters = exp.parameters.dup
32
- @parameters.extend MethodParameters
33
- @num_statements = 0
34
- @refs = AST::ObjectRefs.new
35
- end
36
-
37
- def count_statements(num)
38
- @num_statements += num
39
- end
40
-
41
- def record_call_to(exp)
42
- receiver, meth = exp[1..2]
43
- receiver ||= [:self]
44
- case receiver[0]
45
- when :lvasgn
46
- @refs.record_reference_to(receiver.name, line: exp.line)
47
- when :lvar
48
- @refs.record_reference_to(receiver.name, line: exp.line) unless meth == :new
49
- when :self
50
- @refs.record_reference_to(:self, line: exp.line)
51
- end
52
- end
53
-
54
- def record_use_of_self
55
- @refs.record_reference_to(:self)
56
- end
57
11
 
58
12
  def envious_receivers
59
- return {} if @refs.self_is_max?
60
- @refs.most_popular
13
+ return {} if refs.self_is_max?
14
+ refs.most_popular
61
15
  end
62
16
 
63
17
  def references_self?
@@ -79,6 +33,15 @@ module Reek
79
33
  def uses_super_with_implicit_arguments?
80
34
  (body = exp.body) && body.contains_nested_node?(:zsuper)
81
35
  end
36
+
37
+ def default_assignments
38
+ @default_assignments ||=
39
+ exp.parameters.select(&:optional_argument?).map(&:children)
40
+ end
41
+
42
+ def singleton_method?
43
+ exp.singleton_method? || visibility == :module_function
44
+ end
82
45
  end
83
46
  end
84
47
  end
@@ -8,11 +8,6 @@ module Reek
8
8
  #
9
9
  # @api private
10
10
  class ModuleContext < CodeContext
11
- def initialize(outer, exp)
12
- super(outer, exp)
13
- @name = AST::SexpFormatter.format(exp.children.first)
14
- end
15
-
16
11
  def node_instance_methods
17
12
  local_nodes(:def)
18
13
  end
@@ -28,8 +23,9 @@ module Reek
28
23
  #
29
24
  # @return true if the module is a namespace module
30
25
  def namespace_module?
26
+ return false if exp.type == :casgn
31
27
  contents = exp.children.last
32
- contents && contents.find_nodes([:def, :defs], [:class, :module]).empty?
28
+ contents && contents.find_nodes([:def, :defs], [:casgn, :class, :module]).empty?
33
29
  end
34
30
  end
35
31
  end
@@ -1,25 +1,18 @@
1
+ require_relative 'code_context'
2
+
1
3
  module Reek
2
- # @api private
3
4
  module Context
4
5
  #
5
6
  # A context wrapper representing the root of an abstract syntax tree.
6
7
  #
7
8
  # @api private
8
- class RootContext
9
- def initialize
10
- @name = ''
11
- end
12
-
13
- def method_missing(_method, *_args)
14
- nil
15
- end
16
-
17
- def config_for(_)
18
- {}
9
+ class RootContext < CodeContext
10
+ def initialize(exp)
11
+ super(nil, exp)
19
12
  end
20
13
 
21
- def count_statements(_num)
22
- 0
14
+ def type
15
+ :root
23
16
  end
24
17
 
25
18
  def full_name
data/lib/reek/examiner.rb CHANGED
@@ -21,13 +21,16 @@ module Reek
21
21
  # If +source+ is a String it is assumed to be Ruby source code;
22
22
  # if it is a File or IO, it is opened and Ruby source code is read from it;
23
23
  #
24
- # @param smell_types_to_filter_by [Array<String>]
24
+ # @param filter_by_smells [Array<String>]
25
25
  # List of smell types to filter by.
26
26
  #
27
- def initialize(source, smell_types_to_filter_by = [])
28
- @source = Source::SourceCode.from(source)
29
- @collector = CLI::WarningCollector.new
30
- @smell_types = eligible_smell_types(smell_types_to_filter_by)
27
+ def initialize(source,
28
+ filter_by_smells = [],
29
+ configuration: Configuration::AppConfiguration.new)
30
+ @source = Source::SourceCode.from(source)
31
+ @configuration = configuration
32
+ @collector = CLI::WarningCollector.new
33
+ @smell_types = eligible_smell_types(filter_by_smells)
31
34
 
32
35
  run
33
36
  end
@@ -63,16 +66,18 @@ module Reek
63
66
  private
64
67
 
65
68
  def run
66
- smell_repository = Smells::SmellRepository.new(description, @smell_types)
69
+ smell_repository = Smells::SmellRepository.new(source_description: description,
70
+ smell_types: @smell_types,
71
+ configuration: @configuration)
67
72
  syntax_tree = @source.syntax_tree
68
- TreeWalker.new(smell_repository).process(syntax_tree) if syntax_tree
73
+ TreeWalker.new(smell_repository, syntax_tree).walk if syntax_tree
69
74
  smell_repository.report_on(@collector)
70
75
  end
71
76
 
72
- def eligible_smell_types(smell_types_to_filter_by = [])
73
- if smell_types_to_filter_by.any?
77
+ def eligible_smell_types(filter_by_smells = [])
78
+ if filter_by_smells.any?
74
79
  Smells::SmellRepository.smell_types.select do |klass|
75
- smell_types_to_filter_by.include? klass.smell_type
80
+ filter_by_smells.include? klass.smell_type
76
81
  end
77
82
  else
78
83
  Smells::SmellRepository.smell_types
@@ -1,5 +1,6 @@
1
- require 'rainbow'
2
1
  require 'json'
2
+ require 'pathname'
3
+ require 'rainbow'
3
4
  require_relative 'formatter'
4
5
  require_relative 'heading_formatter'
5
6
 
@@ -129,9 +130,9 @@ module Reek
129
130
  class HTMLReport < Base
130
131
  require 'erb'
131
132
 
132
- def show(target_path = 'reek.html')
133
- template_path = File.expand_path('../html_report.html.erb', __FILE__)
134
- File.write target_path, ERB.new(File.read(template_path)).result(binding)
133
+ def show(target_path = Pathname.new('reek.html'))
134
+ template_path = Pathname.new("#{__dir__}/html_report.html.erb")
135
+ File.write target_path, ERB.new(template_path.read).result(binding)
135
136
  puts 'HTML file saved'
136
137
  end
137
138
  end
@@ -24,7 +24,7 @@ module Reek
24
24
  # @return [Array<SmellWarning>]
25
25
  #
26
26
  def examine_context(method_ctx)
27
- method_ctx.parameters.default_assignments.select do |_param, value|
27
+ method_ctx.default_assignments.select do |_param, value|
28
28
  [:true, :false].include?(value[0])
29
29
  end.map do |parameter, _value|
30
30
  SmellWarning.new self,
@@ -108,7 +108,7 @@ module Reek
108
108
 
109
109
  def collect_calls(result)
110
110
  context.each_node(:send, [:mlhs]) do |call_node|
111
- next if initializer_call? call_node
111
+ next if call_node.object_creation_call?
112
112
  next if simple_method_call? call_node
113
113
  result[call_node].record(call_node)
114
114
  end
@@ -125,10 +125,6 @@ module Reek
125
125
  !call_node.receiver && call_node.args.empty?
126
126
  end
127
127
 
128
- def initializer_call?(call_node)
129
- call_node.method_name == :new
130
- end
131
-
132
128
  def allow_calls?(method)
133
129
  @allow_calls.any? { |allow| /#{allow}/ =~ method }
134
130
  end
@@ -11,7 +11,7 @@ module Reek
11
11
  # @api private
12
12
  class IrresponsibleModule < SmellDetector
13
13
  def self.contexts
14
- [:class, :module]
14
+ [:casgn, :class, :module]
15
15
  end
16
16
 
17
17
  #
@@ -26,7 +26,7 @@ module Reek
26
26
  context: ctx.full_name,
27
27
  lines: [expression.line],
28
28
  message: 'has no descriptive comment',
29
- parameters: { name: expression.text_name })]
29
+ parameters: { name: expression.name })]
30
30
  end
31
31
 
32
32
  private
@@ -9,45 +9,59 @@ module Reek
9
9
  #
10
10
  # @api private
11
11
  class SmellRepository
12
- attr_reader :detectors
13
-
14
12
  def self.smell_types
15
13
  Reek::Smells::SmellDetector.descendants.sort_by(&:name)
16
14
  end
17
15
 
18
- def initialize(source_description = nil, smell_types = self.class.smell_types)
19
- @typed_detectors = nil
20
- @detectors = {}
21
- smell_types.each do |klass|
22
- @detectors[klass] = klass.new(source_description)
16
+ def initialize(source_description: nil,
17
+ smell_types: self.class.smell_types,
18
+ configuration: Configuration::AppConfiguration.new)
19
+ self.source_via = source_description
20
+ self.typed_detectors = nil
21
+ self.configuration = configuration
22
+ self.smell_types = smell_types
23
+
24
+ configuration.directive_for(source_via).each do |klass, config|
25
+ configure klass, config
23
26
  end
24
- Configuration::AppConfiguration.configure_smell_repository self
25
27
  end
26
28
 
27
29
  def configure(klass, config)
28
- detector = @detectors[klass]
30
+ detector = detectors[klass]
29
31
  raise ArgumentError, "Unknown smell type #{klass} found in configuration" unless detector
30
32
  detector.configure_with(config)
31
33
  end
32
34
 
33
35
  def report_on(listener)
34
- @detectors.each_value { |detector| detector.report_on(listener) }
36
+ detectors.each_value { |detector| detector.report_on(listener) }
35
37
  end
36
38
 
37
- def examine(scope, node_type)
38
- smell_listeners[node_type].each do |detector|
39
+ def examine(scope)
40
+ smell_listeners[scope.type].each do |detector|
39
41
  detector.examine(scope)
40
42
  end
41
43
  end
42
44
 
45
+ def detectors
46
+ @initialized_detectors ||= begin
47
+ @detectors = {}
48
+ smell_types.each do |klass|
49
+ @detectors[klass] = klass.new(source_via)
50
+ end
51
+ @detectors
52
+ end
53
+ end
54
+
43
55
  private
44
56
 
57
+ attr_accessor :typed_detectors, :configuration, :source_via, :smell_types
58
+
45
59
  def smell_listeners
46
- unless @typed_detectors
47
- @typed_detectors = Hash.new { |hash, key| hash[key] = [] }
48
- @detectors.each_value { |detector| detector.register(@typed_detectors) }
60
+ unless typed_detectors
61
+ self.typed_detectors = Hash.new { |hash, key| hash[key] = [] }
62
+ detectors.each_value { |detector| detector.register(typed_detectors) }
49
63
  end
50
- @typed_detectors
64
+ typed_detectors
51
65
  end
52
66
  end
53
67
  end
@@ -54,7 +54,7 @@ module Reek
54
54
  # @return [Array<SmellWarning>]
55
55
  #
56
56
  def examine_context(method_ctx)
57
- return [] if method_ctx.exp.singleton_method?
57
+ return [] if method_ctx.singleton_method?
58
58
  return [] if method_ctx.num_statements == 0
59
59
  return [] if method_ctx.references_self?
60
60
  return [] if num_helper_methods(method_ctx).zero?
@@ -13,12 +13,15 @@ module Reek
13
13
  #
14
14
  # @api private
15
15
  class SourceCode
16
+ IO_IDENTIFIER = 'STDIN'
17
+ STRING_IDENTIFIER = 'string'
18
+
16
19
  attr_reader :description
17
20
 
18
21
  # Initializer.
19
22
  #
20
23
  # code - ruby code as String
21
- # description - in case of STDIN this is "STDIN" otherwise it's a filepath as String
24
+ # description - 'STDIN', 'string' or a filepath as String
22
25
  # parser - the parser to use for generating AST's out of the given source
23
26
  def initialize(code, description, parser = Parser::Ruby22)
24
27
  @source = code
@@ -27,8 +30,8 @@ module Reek
27
30
  end
28
31
 
29
32
  # Initializes an instance of SourceCode given a source.
30
- # This source can come via 3 different ways:
31
- # - from files a la `reek lib/reek/`
33
+ # This source can come via 4 different ways:
34
+ # - from Files or Pathnames a la `reek lib/reek/`
32
35
  # - from IO (STDIN) a la `echo "class Foo; end" | reek`
33
36
  # - from String via our rspec matchers a la `expect("class Foo; end").to reek`
34
37
  #
@@ -37,9 +40,10 @@ module Reek
37
40
  # @return an instance of SourceCode
38
41
  def self.from(source)
39
42
  case source
40
- when File then new(source.read, source.path)
41
- when IO then new(source.readlines.join, 'STDIN')
42
- when String then new(source, 'string')
43
+ when File then new(source.read, source.path)
44
+ when IO then new(source.readlines.join, IO_IDENTIFIER)
45
+ when Pathname then new(source.read, source.to_s)
46
+ when String then new(source, STRING_IDENTIFIER)
43
47
  end
44
48
  end
45
49
 
@@ -1,4 +1,5 @@
1
1
  require 'find'
2
+ require 'pathname'
2
3
 
3
4
  module Reek
4
5
  module Source
@@ -10,61 +11,61 @@ module Reek
10
11
  # Initialize with the paths we want to search.
11
12
  #
12
13
  # paths - a list of paths as Strings
13
- def initialize(paths)
14
- @pathnames = paths.
15
- map { |path| Pathname.new(path.chomp('/')) }.
16
- flat_map { |pathname| current_directory?(pathname) ? pathname.entries : pathname }
14
+ def initialize(paths, configuration: Configuration::AppConfiguration.new)
15
+ self.paths = paths.flat_map do |string|
16
+ path = Pathname.new(string)
17
+ current_directory?(path) ? path.entries : path
18
+ end
19
+ self.configuration = configuration
17
20
  end
18
21
 
19
22
  # Traverses all paths we initialized the SourceLocator with, finds
20
23
  # all relevant ruby files and returns them as a list.
21
24
  #
22
- # @return [Array<File>] - Ruby files found
25
+ # @return [Array<Pathname>] - Ruby paths found
23
26
  def sources
24
- source_paths.map { |pathname| File.new(pathname) }
27
+ source_paths
25
28
  end
26
29
 
27
30
  private
28
31
 
32
+ attr_accessor :paths, :configuration
33
+
29
34
  def source_paths
30
- @pathnames.each_with_object([]) do |given_pathname, relevant_paths|
31
- print_no_such_file_error(given_pathname) && next unless path_exists?(given_pathname)
32
- given_pathname.find do |pathname|
33
- if pathname.directory?
34
- ignore_path?(pathname) ? Find.prune : next
35
+ paths.each_with_object([]) do |given_path, relevant_paths|
36
+ print_no_such_file_error(given_path) && next unless given_path.exist?
37
+ given_path.find do |path|
38
+ if path.directory?
39
+ ignore_path?(path) ? Find.prune : next
35
40
  else
36
- relevant_paths << pathname if ruby_file?(pathname)
41
+ relevant_paths << path if ruby_file?(path)
37
42
  end
38
43
  end
39
44
  end
40
45
  end
41
46
 
42
- def path_excluded?(pathname)
43
- Configuration::AppConfiguration.exclude_paths.include? pathname.to_s
44
- end
45
-
46
- def path_exists?(path)
47
- Pathname.new(path).exist?
47
+ def path_excluded?(path)
48
+ configuration.exclude_paths.include?(path)
48
49
  end
49
50
 
50
51
  def print_no_such_file_error(path)
51
52
  $stderr.puts "Error: No such file - #{path}"
52
53
  end
53
54
 
54
- def hidden_directory?(pathname)
55
- pathname.basename.to_s.start_with? '.'
55
+ def hidden_directory?(path)
56
+ path.basename.to_s.start_with? '.'
56
57
  end
57
58
 
58
- def ignore_path?(pathname)
59
- path_excluded?(pathname) || hidden_directory?(pathname)
59
+ def ignore_path?(path)
60
+ path_excluded?(path) || hidden_directory?(path)
60
61
  end
61
62
 
62
- def ruby_file?(pathname)
63
- pathname.extname == '.rb'
63
+ def ruby_file?(path)
64
+ path.extname == '.rb'
64
65
  end
65
66
 
66
- def current_directory?(pathname)
67
- ['.', './'].include? pathname.to_s
67
+ def current_directory?(path)
68
+ [Pathname.new('.'), Pathname.new('./')].include?(path)
68
69
  end
69
70
  end
70
71
  end