reek 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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