reek 1.4.0 → 1.5.0

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +5 -0
  3. data/README.md +70 -92
  4. data/config/defaults.reek +3 -0
  5. data/features/samples.feature +24 -20
  6. data/features/step_definitions/reek_steps.rb +1 -1
  7. data/features/support/env.rb +7 -7
  8. data/lib/reek/core/code_context.rb +1 -1
  9. data/lib/reek/core/code_parser.rb +19 -18
  10. data/lib/reek/core/method_context.rb +8 -7
  11. data/lib/reek/core/module_context.rb +1 -1
  12. data/lib/reek/core/smell_repository.rb +1 -0
  13. data/lib/reek/core/sniffer.rb +3 -1
  14. data/lib/reek/rake/task.rb +1 -5
  15. data/lib/reek/smell_description.rb +26 -0
  16. data/lib/reek/smell_warning.rb +35 -49
  17. data/lib/reek/smells.rb +1 -0
  18. data/lib/reek/smells/attribute.rb +1 -1
  19. data/lib/reek/smells/control_parameter.rb +14 -7
  20. data/lib/reek/smells/data_clump.rb +1 -1
  21. data/lib/reek/smells/duplicate_method_call.rb +2 -9
  22. data/lib/reek/smells/module_initialize.rb +38 -0
  23. data/lib/reek/smells/nested_iterators.rb +1 -1
  24. data/lib/reek/smells/nil_check.rb +3 -3
  25. data/lib/reek/smells/repeated_conditional.rb +3 -2
  26. data/lib/reek/smells/smell_detector.rb +1 -1
  27. data/lib/reek/smells/too_many_instance_variables.rb +1 -1
  28. data/lib/reek/smells/too_many_methods.rb +1 -1
  29. data/lib/reek/smells/uncommunicative_method_name.rb +0 -4
  30. data/lib/reek/smells/uncommunicative_parameter_name.rb +0 -4
  31. data/lib/reek/smells/uncommunicative_variable_name.rb +11 -9
  32. data/lib/reek/smells/utility_function.rb +2 -2
  33. data/lib/reek/source/ast_node.rb +40 -0
  34. data/lib/reek/source/ast_node_class_map.rb +37 -0
  35. data/lib/reek/source/reference_collector.rb +3 -3
  36. data/lib/reek/source/sexp_extensions.rb +133 -59
  37. data/lib/reek/source/sexp_formatter.rb +10 -4
  38. data/lib/reek/source/sexp_node.rb +25 -17
  39. data/lib/reek/source/source_code.rb +21 -9
  40. data/lib/reek/source/tree_dresser.rb +10 -33
  41. data/lib/reek/version.rb +1 -1
  42. data/reek.gemspec +2 -4
  43. data/spec/matchers/smell_of_matcher.rb +9 -1
  44. data/spec/quality/reek_source_spec.rb +0 -35
  45. data/spec/reek/core/code_context_spec.rb +22 -8
  46. data/spec/reek/core/method_context_spec.rb +10 -10
  47. data/spec/reek/smell_description_spec.rb +43 -0
  48. data/spec/reek/smell_warning_spec.rb +0 -3
  49. data/spec/reek/smells/control_parameter_spec.rb +24 -0
  50. data/spec/reek/smells/feature_envy_spec.rb +50 -17
  51. data/spec/reek/smells/irresponsible_module_spec.rb +25 -17
  52. data/spec/reek/smells/module_initialize_spec.rb +20 -0
  53. data/spec/reek/smells/prima_donna_method_spec.rb +2 -2
  54. data/spec/reek/smells/repeated_conditional_spec.rb +10 -4
  55. data/spec/reek/smells/too_many_instance_variables_spec.rb +47 -21
  56. data/spec/reek/smells/too_many_statements_spec.rb +11 -1
  57. data/spec/reek/smells/uncommunicative_variable_name_spec.rb +1 -1
  58. data/spec/reek/smells/utility_function_spec.rb +26 -25
  59. data/spec/reek/source/sexp_extensions_spec.rb +164 -91
  60. data/spec/reek/source/sexp_formatter_spec.rb +13 -1
  61. data/spec/reek/source/sexp_node_spec.rb +5 -5
  62. data/spec/reek/source/source_code_spec.rb +18 -6
  63. data/spec/reek/source/tree_dresser_spec.rb +5 -5
  64. data/spec/spec_helper.rb +8 -4
  65. metadata +16 -50
@@ -1,4 +1,6 @@
1
- require 'ruby2ruby'
1
+ old_verbose, $VERBOSE = $VERBOSE, nil
2
+ require 'unparser'
3
+ $VERBOSE = old_verbose
2
4
 
3
5
  module Reek
4
6
  module Source
@@ -7,9 +9,13 @@ module Reek
7
9
  #
8
10
  class SexpFormatter
9
11
  def self.format(sexp)
10
- return sexp.to_s unless sexp.is_a? Array
11
- sexp = Sexp.from_array(YAML.load(YAML.dump(sexp)))
12
- Ruby2Ruby.new.process(sexp)
12
+ return sexp.to_s unless sexp.is_a? AST::Node
13
+ lines = Unparser.unparse(sexp).split "\n"
14
+ if lines.length > 1
15
+ "#{lines.first} ... #{lines.last}"
16
+ else
17
+ lines.first
18
+ end
13
19
  end
14
20
  end
15
21
  end
@@ -7,7 +7,7 @@ module Reek
7
7
  module SexpNode
8
8
  def self.format(expr)
9
9
  case expr
10
- when Sexp then expr.format_ruby
10
+ when AST::Node then expr.format_ruby
11
11
  else expr.to_s
12
12
  end
13
13
  end
@@ -26,20 +26,14 @@ module Reek
26
26
  end
27
27
  end
28
28
 
29
- def unnested_nodes(types)
29
+ def find_nodes(types, ignoring = [])
30
30
  result = []
31
- if types.include? first
32
- result << self
33
- else
34
- each_sexp do |elem|
35
- result += elem.unnested_nodes(types)
36
- end
37
- end
31
+ look_for_alt(types, ignoring) { |exp| result << exp }
38
32
  result
39
33
  end
40
34
 
41
35
  def each_sexp
42
- each { |elem| yield elem if elem.is_a? Sexp }
36
+ children.each { |elem| yield elem if elem.is_a? AST::Node }
43
37
  end
44
38
 
45
39
  #
@@ -49,9 +43,27 @@ module Reek
49
43
  #
50
44
  def look_for(target_type, ignoring = [], &blk)
51
45
  each_sexp do |elem|
52
- elem.look_for(target_type, ignoring, &blk) unless ignoring.include?(elem.first)
46
+ elem.look_for(target_type, ignoring, &blk) unless ignoring.include?(elem.type)
47
+ end
48
+ blk.call(self) if type == target_type
49
+ end
50
+
51
+ #
52
+ # Carries out a depth-first traversal of this syntax tree, yielding
53
+ # every Sexp of type +target_type+. The traversal ignores any node
54
+ # whose type is listed in the Array +ignoring+, includeing the top node.
55
+ #
56
+ # Also, doesn't nest
57
+ #
58
+ def look_for_alt(target_types, ignoring = [], &blk)
59
+ return if ignoring.include?(type)
60
+ if target_types.include? type
61
+ blk.call(self)
62
+ else
63
+ each_sexp do |elem|
64
+ elem.look_for_alt(target_types, ignoring, &blk)
65
+ end
53
66
  end
54
- blk.call(self) if first == target_type
55
67
  end
56
68
 
57
69
  def contains_nested_node?(target_type)
@@ -60,11 +72,7 @@ module Reek
60
72
  end
61
73
 
62
74
  def format_ruby
63
- Ruby2Ruby.new.process(deep_copy)
64
- end
65
-
66
- def deep_copy
67
- Sexp.new(*map { |elem| elem.is_a?(Sexp) ? elem.deep_copy : elem })
75
+ SexpFormatter.format(self)
68
76
  end
69
77
  end
70
78
  end
@@ -1,6 +1,9 @@
1
- require 'ruby_parser'
1
+ old_verbose, $VERBOSE = $VERBOSE, nil
2
+ require 'parser/current'
3
+ $VERBOSE = old_verbose
2
4
  require 'reek/source/config_file'
3
5
  require 'reek/source/tree_dresser'
6
+ require 'reek/source/ast_node'
4
7
 
5
8
  module Reek
6
9
  module Source
@@ -10,7 +13,7 @@ module Reek
10
13
  class SourceCode
11
14
  attr_reader :desc
12
15
 
13
- def initialize(code, desc, parser = RubyParser.new)
16
+ def initialize(code, desc, parser = Parser::Ruby21.new)
14
17
  @source = code
15
18
  @desc = desc
16
19
  @parser = parser
@@ -21,13 +24,22 @@ module Reek
21
24
  end
22
25
 
23
26
  def syntax_tree
24
- begin
25
- ast = @parser.parse(@source, @desc)
26
- rescue Racc::ParseError, RubyParser::SyntaxError => error
27
- $stderr.puts "#{desc}: #{error.class.name}: #{error}"
28
- end
29
- ast ||= s()
30
- TreeDresser.new.dress(ast)
27
+ @syntax_tree ||=
28
+ begin
29
+ buffer = Parser::Source::Buffer.new(@desc)
30
+ buffer.source = @source
31
+
32
+ begin
33
+ ast, comments = @parser.parse_with_comments(buffer)
34
+ rescue Racc::ParseError, Parser::SyntaxError => error
35
+ $stderr.puts "#{desc}: #{error.class.name}: #{error}"
36
+ end
37
+ ast ||= AstNode.new(:empty)
38
+ comments ||= []
39
+
40
+ comment_map = Parser::Source::Comment.associate(ast, comments)
41
+ TreeDresser.new.dress(ast, comment_map)
42
+ end
31
43
  end
32
44
  end
33
45
  end
@@ -1,5 +1,4 @@
1
- require 'reek/source/sexp_node'
2
- require 'reek/source/sexp_extensions'
1
+ require 'reek/source/ast_node_class_map'
3
2
 
4
3
  module Reek
5
4
  module Source
@@ -8,39 +7,17 @@ module Reek
8
7
  # the tree more understandable and less implementation-dependent.
9
8
  #
10
9
  class TreeDresser
11
- def initialize(extensions_module = SexpExtensions, node_module = SexpNode)
12
- @extensions_module = extensions_module
13
- @node_module = node_module
10
+ def initialize(klass_map = AstNodeClassMap.new)
11
+ @klass_map = klass_map
14
12
  end
15
13
 
16
- def dress(sexp)
17
- extend_sexp(sexp)
18
- sexp.each_sexp { |sub| dress(sub) }
19
- sexp
20
- end
21
-
22
- private
23
-
24
- def extend_sexp(sexp)
25
- sexp.extend(@node_module)
26
- extension_module = extension_for(sexp)
27
- sexp.extend(extension_module) if extension_module
28
- end
29
-
30
- def extension_for(sexp)
31
- extension_map[sexp.sexp_type]
32
- end
33
-
34
- def extension_map
35
- @extension_map ||= begin
36
- assoc = @extensions_module.constants.map do |const|
37
- [
38
- const.to_s.sub(/Node$/, '').downcase.to_sym,
39
- @extensions_module.const_get(const)
40
- ]
41
- end
42
- Hash[assoc]
43
- end
14
+ def dress(sexp, comment_map)
15
+ return sexp unless sexp.is_a? AST::Node
16
+ type = sexp.type
17
+ children = sexp.children.map { |child| dress(child, comment_map) }
18
+ comments = comment_map[sexp]
19
+ @klass_map.klass_for(type).new(type, children,
20
+ location: sexp.loc, comments: comments)
44
21
  end
45
22
  end
46
23
  end
data/lib/reek/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Reek
2
- VERSION = '1.4.0'
2
+ VERSION = '1.5.0'
3
3
  end
data/reek.gemspec CHANGED
@@ -30,15 +30,13 @@ Gem::Specification.new do |s|
30
30
  s.required_ruby_version = '>= 1.9.2'
31
31
  s.summary = 'Code smell detector for Ruby'
32
32
 
33
- s.add_runtime_dependency('ruby_parser', ['>= 3.5.0', '< 4.0'])
34
- s.add_runtime_dependency('sexp_processor', ['~> 4.4'])
35
- s.add_runtime_dependency('ruby2ruby', ['>= 2.0.8', '< 3.0'])
33
+ s.add_runtime_dependency('parser', ['~> 2.2.0.pre.7'])
34
+ s.add_runtime_dependency('unparser', ['~> 0.1.16'])
36
35
  s.add_runtime_dependency('rainbow', ['>= 1.99', '< 3.0'])
37
36
 
38
37
  s.add_development_dependency('bundler', ['~> 1.1'])
39
38
  s.add_development_dependency('rake', ['~> 10.0'])
40
39
  s.add_development_dependency('cucumber', ['~> 1.3'])
41
40
  s.add_development_dependency('rspec', ['~> 3.0'])
42
- s.add_development_dependency('flay', ['~> 2.4'])
43
41
  s.add_development_dependency('yard', ['>= 0.8.7', '< 0.9'])
44
42
  end
@@ -34,7 +34,15 @@ module SmellOfMatcher
34
34
  private
35
35
 
36
36
  def detect_smells
37
- ctx = MethodContext.new(nil, @source.syntax_tree)
37
+ tree = @source.syntax_tree
38
+ ctx = case tree.type
39
+ when :def, :defs
40
+ MethodContext.new(nil, tree)
41
+ when :module, :class
42
+ ModuleContext.new(nil, tree)
43
+ else
44
+ CodeContext.new(nil, tree)
45
+ end
38
46
  detector = @klass.new(@source.desc, @klass.default_config.merge(@config))
39
47
  detector.examine(ctx)
40
48
  @actual_smells = detector.smells_found.to_a
@@ -1,42 +1,7 @@
1
1
  require 'spec_helper'
2
- require 'flay'
3
-
4
- RSpec::Matchers.define :flay do |threshold|
5
- match do |dirs_and_files|
6
- @threshold = threshold
7
- @flay = Flay.new(fuzzy: false, verbose: false, mass: @threshold)
8
- @flay.process(*Flay.expand_dirs_to_files(dirs_and_files))
9
- @flay.total > 0
10
- end
11
-
12
- failure_message_for_should do
13
- "Expected source to contain duplication, but it didn't"
14
- end
15
-
16
- failure_message_for_should_not do
17
- "Expected source not to contain duplication, but got:\n#{report}"
18
- end
19
-
20
- def report
21
- lines = ["Total mass = #{@flay.total} (threshold = #{@threshold})"]
22
- @flay.masses.each do |hash, mass|
23
- nodes = @flay.hashes[hash]
24
- match = @flay.identical[hash] ? 'IDENTICAL' : 'Similar'
25
- lines << format('%s code found in %p (%d)', match, nodes.first.first, mass)
26
- nodes.each { |x| lines << " #{x.file}:#{x.line}" }
27
- end
28
- lines.join("\n")
29
- end
30
- end
31
2
 
32
3
  describe 'Reek source code' do
33
4
  it 'has no smells' do
34
5
  Dir['lib/**/*.rb'].should_not reek
35
6
  end
36
- it 'has no structural duplication' do
37
- ['lib'].should_not flay(16)
38
- end
39
- it 'has no structural duplication in the tests' do
40
- ['spec/reek'].should_not flay(25)
41
- end
42
7
  end
@@ -56,8 +56,8 @@ describe CodeContext do
56
56
  it 'should pass unknown method calls down the stack' do
57
57
  stop = StopContext.new
58
58
  def stop.bananas(arg1, arg2) arg1 + arg2 + 43 end
59
- element = ModuleContext.new(stop, ast(:module, :mod, nil))
60
- element = MethodContext.new(element, ast(:defn, :bad))
59
+ element = ModuleContext.new(stop, s(:module, :mod, nil))
60
+ element = MethodContext.new(element, s(:def, :bad, s(:args), nil))
61
61
  expect(element.bananas(17, -5)).to eq(55)
62
62
  end
63
63
  end
@@ -70,16 +70,21 @@ describe CodeContext do
70
70
  ast = src.to_reek_source.syntax_tree
71
71
  @ctx = CodeContext.new(nil, ast)
72
72
  end
73
+
73
74
  it 'yields no calls' do
74
- @ctx.each_node(:call, []) { |exp| raise "#{exp} yielded by empty module!" }
75
+ @ctx.each_node(:send, []) { |exp| raise "#{exp} yielded by empty module!" }
75
76
  end
77
+
76
78
  it 'yields one module' do
77
79
  mods = 0
78
80
  @ctx.each_node(:module, []) { |_exp| mods += 1 }
79
81
  expect(mods).to eq(1)
80
82
  end
83
+
81
84
  it "yields the module's full AST" do
82
- @ctx.each_node(:module, []) { |exp| expect(exp[1]).to eq(@module_name.to_sym) }
85
+ @ctx.each_node(:module, []) do |exp|
86
+ expect(exp).to eq(s(:module, s(:const, nil, @module_name.to_sym), nil))
87
+ end
83
88
  end
84
89
 
85
90
  context 'with no block' do
@@ -103,19 +108,28 @@ describe CodeContext do
103
108
  it 'yields one module' do
104
109
  expect(@ctx.each_node(:module, []).length).to eq(1)
105
110
  end
111
+
106
112
  it "yields the module's full AST" do
107
- @ctx.each_node(:module, []) { |exp| expect(exp[1]).to eq(@module_name.to_sym) }
113
+ @ctx.each_node(:module, []) do |exp|
114
+ expect(exp).to eq s(:module,
115
+ s(:const, nil, @module_name.to_sym),
116
+ s(:def, :calloo,
117
+ s(:args),
118
+ s(:send, nil, :puts, s(:str, 'hello'))))
119
+ end
108
120
  end
121
+
109
122
  it 'yields one method' do
110
- expect(@ctx.each_node(:defn, []).length).to eq(1)
123
+ expect(@ctx.each_node(:def, []).length).to eq(1)
111
124
  end
125
+
112
126
  it "yields the method's full AST" do
113
- @ctx.each_node(:defn, []) { |exp| expect(exp[1]).to eq(@method_name.to_sym) }
127
+ @ctx.each_node(:def, []) { |exp| expect(exp[1]).to eq(@method_name.to_sym) }
114
128
  end
115
129
 
116
130
  context 'pruning the traversal' do
117
131
  it 'ignores the call inside the method' do
118
- expect(@ctx.each_node(:call, [:defn])).to be_empty
132
+ expect(@ctx.each_node(:send, [:def])).to be_empty
119
133
  end
120
134
  end
121
135
  end
@@ -24,23 +24,23 @@ end
24
24
 
25
25
  describe MethodContext do
26
26
  it 'should record ivars as refs to self' do
27
- mctx = MethodContext.new(StopContext.new, ast(:defn, :feed))
27
+ mctx = MethodContext.new(StopContext.new, s(:def, :feed, s(:args), nil))
28
28
  expect(mctx.envious_receivers).to eq([])
29
- mctx.record_call_to(ast(:call, s(:ivar, :@cow), :feed_to))
29
+ mctx.record_call_to(s(:send, s(:ivar, :@cow), :feed_to))
30
30
  expect(mctx.envious_receivers).to eq([])
31
31
  end
32
32
 
33
33
  it 'should count calls to self' do
34
- mctx = MethodContext.new(StopContext.new, ast(:defn, :equals))
34
+ mctx = MethodContext.new(StopContext.new, s(:def, :equals, s(:args), nil))
35
35
  mctx.refs.record_reference_to([:lvar, :other])
36
- mctx.record_call_to(ast(:call, s(:self), :thing))
36
+ mctx.record_call_to(s(:send, s(:self), :thing))
37
37
  expect(mctx.envious_receivers).to be_empty
38
38
  end
39
39
 
40
40
  it 'should recognise a call on self' do
41
- mc = MethodContext.new(StopContext.new, s(:defn, :deep))
42
- mc.record_call_to(ast(:call, s(:lvar, :text), :each, s(:arglist)))
43
- mc.record_call_to(ast(:call, nil, :shelve, s(:arglist)))
41
+ mc = MethodContext.new(StopContext.new, s(:def, :deep, s(:args), nil))
42
+ mc.record_call_to(s(:send, s(:lvar, :text), :each, s(:arglist)))
43
+ mc.record_call_to(s(:send, nil, :shelve, s(:arglist)))
44
44
  expect(mc.envious_receivers).to be_empty
45
45
  end
46
46
  end
@@ -65,7 +65,7 @@ describe MethodParameters, 'default assignments' do
65
65
  @defaults = assignments_from(src)
66
66
  end
67
67
  it 'returns the param-value pair' do
68
- expect(@defaults[0]).to eq(s(:argb, s(:lit, 456)))
68
+ expect(@defaults[0]).to eq [:argb, s(:int, 456)]
69
69
  end
70
70
  it 'returns the nothing else' do
71
71
  expect(@defaults.length).to eq(1)
@@ -78,8 +78,8 @@ describe MethodParameters, 'default assignments' do
78
78
  @defaults = assignments_from(src)
79
79
  end
80
80
  it 'returns both param-value pairs' do
81
- expect(@defaults[0]).to eq(s(:arga, s(:lit, 123)))
82
- expect(@defaults[1]).to eq(s(:argb, s(:lit, 456)))
81
+ expect(@defaults[0]).to eq [:arga, s(:int, 123)]
82
+ expect(@defaults[1]).to eq [:argb, s(:int, 456)]
83
83
  end
84
84
  it 'returns nothing else' do
85
85
  expect(@defaults.length).to eq(2)
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'reek/smell_description'
3
+
4
+ describe Reek::SmellDescription do
5
+ let(:smell_class) { 'SmellClass' }
6
+ let(:smell_subclass) { 'SmellySubclass' }
7
+ let(:message) { 'smell message' }
8
+ let(:details) { { 'key1' => 'value1', 'key2' => 'value2' } }
9
+
10
+ let(:description) { described_class.new(smell_class, smell_subclass, message, details) }
11
+
12
+ it "knows its smell class" do
13
+ expect(description.smell_class).to eq smell_class
14
+ end
15
+
16
+ it "knows its smell subclass" do
17
+ expect(description.smell_subclass).to eq smell_subclass
18
+ end
19
+
20
+ it "knows its smell message" do
21
+ expect(description.message).to eq message
22
+ end
23
+
24
+ it "knows its details" do
25
+ expect(description.details).to eq details
26
+ end
27
+
28
+ it "accesses its details through #[]" do
29
+ expect(description['key1']).to eq 'value1'
30
+ expect(description['key2']).to eq 'value2'
31
+ end
32
+
33
+ it "outputs the correct YAML" do
34
+ expect(description.to_yaml).to eq <<-END
35
+ ---
36
+ class: SmellClass
37
+ subclass: SmellySubclass
38
+ message: smell message
39
+ key1: value1
40
+ key2: value2
41
+ END
42
+ end
43
+ end