reek 0.3.1 → 1.0.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 (96) hide show
  1. data/History.txt +20 -0
  2. data/README.txt +4 -80
  3. data/Rakefile +15 -4
  4. data/bin/reek +10 -16
  5. data/config/defaults.reek +53 -0
  6. data/lib/reek.rb +1 -21
  7. data/lib/reek/block_context.rb +37 -0
  8. data/lib/reek/class_context.rb +73 -0
  9. data/lib/reek/code_context.rb +47 -0
  10. data/lib/reek/code_parser.rb +204 -0
  11. data/lib/reek/exceptions.reek +13 -0
  12. data/lib/reek/if_context.rb +25 -0
  13. data/lib/reek/method_context.rb +85 -0
  14. data/lib/reek/module_context.rb +34 -0
  15. data/lib/reek/name.rb +42 -0
  16. data/lib/reek/object_refs.rb +3 -6
  17. data/lib/reek/options.rb +60 -40
  18. data/lib/reek/rake_task.rb +20 -29
  19. data/lib/reek/report.rb +16 -27
  20. data/lib/reek/sexp_formatter.rb +52 -0
  21. data/lib/reek/singleton_method_context.rb +27 -0
  22. data/lib/reek/smell_warning.rb +49 -0
  23. data/lib/reek/smells/control_couple.rb +21 -13
  24. data/lib/reek/smells/duplication.rb +23 -27
  25. data/lib/reek/smells/feature_envy.rb +18 -25
  26. data/lib/reek/smells/large_class.rb +32 -17
  27. data/lib/reek/smells/long_method.rb +24 -16
  28. data/lib/reek/smells/long_parameter_list.rb +25 -18
  29. data/lib/reek/smells/long_yield_list.rb +7 -9
  30. data/lib/reek/smells/nested_iterators.rb +13 -9
  31. data/lib/reek/smells/smell_detector.rb +66 -0
  32. data/lib/reek/smells/smells.rb +71 -10
  33. data/lib/reek/smells/uncommunicative_name.rb +49 -41
  34. data/lib/reek/smells/utility_function.rb +18 -18
  35. data/lib/reek/source.rb +116 -0
  36. data/lib/reek/spec.rb +146 -0
  37. data/lib/reek/stop_context.rb +62 -0
  38. data/lib/reek/yield_call_context.rb +14 -0
  39. data/reek.gemspec +42 -0
  40. data/spec/integration/reek_source_spec.rb +20 -0
  41. data/spec/{script_spec.rb → integration/script_spec.rb} +11 -24
  42. data/spec/reek/class_context_spec.rb +198 -0
  43. data/spec/reek/code_context_spec.rb +92 -0
  44. data/spec/reek/code_parser_spec.rb +44 -0
  45. data/spec/reek/config_spec.rb +42 -0
  46. data/spec/reek/if_context_spec.rb +17 -0
  47. data/spec/reek/method_context_spec.rb +52 -0
  48. data/spec/reek/module_context_spec.rb +38 -0
  49. data/spec/reek/options_spec.rb +2 -28
  50. data/spec/reek/report_spec.rb +6 -40
  51. data/spec/reek/sexp_formatter_spec.rb +31 -0
  52. data/spec/reek/singleton_method_context_spec.rb +17 -0
  53. data/spec/reek/smells/control_couple_spec.rb +10 -18
  54. data/spec/reek/smells/duplication_spec.rb +53 -32
  55. data/spec/reek/smells/feature_envy_spec.rb +87 -49
  56. data/spec/reek/smells/large_class_spec.rb +45 -4
  57. data/spec/reek/smells/long_method_spec.rb +25 -41
  58. data/spec/reek/smells/long_parameter_list_spec.rb +30 -76
  59. data/spec/reek/smells/nested_iterators_spec.rb +19 -29
  60. data/spec/reek/smells/smell_spec.rb +9 -18
  61. data/spec/reek/smells/uncommunicative_name_spec.rb +88 -53
  62. data/spec/reek/smells/utility_function_spec.rb +45 -44
  63. data/spec/samples/inline_spec.rb +40 -0
  64. data/spec/samples/optparse_spec.rb +100 -0
  65. data/spec/samples/redcloth_spec.rb +93 -0
  66. data/spec/spec_helper.rb +3 -1
  67. data/tasks/reek.rake +1 -10
  68. data/tasks/rspec.rake +16 -35
  69. metadata +43 -46
  70. data/lib/reek/checker.rb +0 -66
  71. data/lib/reek/class_checker.rb +0 -25
  72. data/lib/reek/file_checker.rb +0 -20
  73. data/lib/reek/method_checker.rb +0 -198
  74. data/lib/reek/printer.rb +0 -154
  75. data/lib/reek/smells/smell.rb +0 -56
  76. data/lib/reek/version.rb +0 -9
  77. data/setup.rb +0 -1585
  78. data/spec/integration_spec.rb +0 -30
  79. data/spec/reek/class_checker_spec.rb +0 -48
  80. data/spec/reek/method_checker_spec.rb +0 -67
  81. data/spec/reek/printer_spec.rb +0 -30
  82. data/spec/reek_source_spec.rb +0 -12
  83. data/spec/samples/inline.reek +0 -27
  84. data/spec/samples/optparse.reek +0 -79
  85. data/spec/samples/optparse/date.rb +0 -17
  86. data/spec/samples/optparse/shellwords.rb +0 -6
  87. data/spec/samples/optparse/time.rb +0 -10
  88. data/spec/samples/optparse/uri.rb +0 -6
  89. data/spec/samples/optparse/version.rb +0 -70
  90. data/spec/samples/redcloth.reek +0 -65
  91. data/tasks/samples.rake +0 -17
  92. data/website/index.html +0 -71
  93. data/website/index.txt +0 -40
  94. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  95. data/website/stylesheets/screen.css +0 -138
  96. data/website/template.rhtml +0 -48
@@ -1,6 +1,5 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
- require 'reek/smells/smell'
1
+ require 'reek/smells/smell_detector'
2
+ require 'reek/smell_warning'
4
3
 
5
4
  module Reek
6
5
  module Smells
@@ -9,27 +8,36 @@ module Reek
9
8
  # A Long Method is any method that has a large number of lines.
10
9
  #
11
10
  # Currently +LongMethod+ reports any method with more than
12
- # +MAX_ALLOWED+ statements.
11
+ # 5 statements.
13
12
  #
14
- class LongMethod < Smell
13
+ class LongMethod < SmellDetector
15
14
 
16
- MAX_ALLOWED = 5
15
+ # The name of the config field that sets the maximum number of
16
+ # statements permitted in any method.
17
+ MAX_ALLOWED_STATEMENTS_KEY = 'max_statements'
17
18
 
18
- def self.examine(method, report)
19
- return if method.name == 'initialize'
20
- num = method.num_statements
21
- report << new(method, num) if num > MAX_ALLOWED
19
+ def self.default_config
20
+ super.adopt(
21
+ MAX_ALLOWED_STATEMENTS_KEY => 5,
22
+ EXCLUDE_KEY => ['initialize']
23
+ )
22
24
  end
23
25
 
24
- def initialize(context, num)
25
- super(context)
26
- @num_stmts = num
26
+ def initialize(config = LongMethod.default_config)
27
+ super
28
+ @max_statements = config[MAX_ALLOWED_STATEMENTS_KEY]
27
29
  end
28
30
 
29
- def detailed_report
30
- "#{@context} has approx #{@num_stmts} statements"
31
+ #
32
+ # Checks the length of the given +method+.
33
+ # Any smells found are added to the +report+.
34
+ #
35
+ def examine_context(method, report)
36
+ num = method.num_statements
37
+ return false if num <= @max_statements
38
+ report << SmellWarning.new(self, method,
39
+ "has approx #{num} statements")
31
40
  end
32
41
  end
33
-
34
42
  end
35
43
  end
@@ -1,6 +1,5 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
- require 'reek/smells/smell'
1
+ require 'reek/smells/smell_detector'
2
+ require 'reek/smell_warning'
4
3
 
5
4
  module Reek
6
5
  module Smells
@@ -10,27 +9,35 @@ module Reek
10
9
  # or two parameters, or when a method yields more than one or
11
10
  # two objects to an associated block.
12
11
  #
13
- # Currently +LongParameterList+ reports any method with more
14
- # than +MAX_ALLOWED+ parameters.
12
+ # Currently +LongParameterList+ reports any method or block with too
13
+ # many parameters.
15
14
  #
16
- class LongParameterList < Smell
17
- MAX_ALLOWED = 3
15
+ class LongParameterList < SmellDetector
18
16
 
19
- def self.count_parameters(exp)
20
- result = exp.length - 1
21
- result -= 1 if Array === exp[-1] and exp[-1][0] == :block
22
- result
23
- end
17
+ # The name of the config field that sets the maximum number of
18
+ # parameters permitted in any method or block.
19
+ MAX_ALLOWED_PARAMS_KEY = 'max_params'
24
20
 
25
- def recognise?(args)
26
- @num_params = LongParameterList.count_parameters(args)
27
- @num_params > MAX_ALLOWED
21
+ def self.default_config
22
+ super.adopt(MAX_ALLOWED_PARAMS_KEY => 3)
28
23
  end
29
24
 
30
- def detailed_report
31
- "#{@context.to_s} has #{@num_params} parameters"
25
+ def initialize(config)
26
+ super
27
+ @max_params = config['max_params']
28
+ @action = 'has'
29
+ end
30
+
31
+ #
32
+ # Checks the number of parameters in the given scope.
33
+ # Any smells found are added to the +report+.
34
+ #
35
+ def examine_context(ctx, report)
36
+ num_params = ctx.parameters.length
37
+ return false if num_params <= @max_params
38
+ report << SmellWarning.new(self, ctx,
39
+ "#{@action} #{num_params} parameters")
32
40
  end
33
41
  end
34
-
35
42
  end
36
43
  end
@@ -1,20 +1,18 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
- require 'reek/smells/smell'
1
+ require 'reek/smells/smell_detector'
4
2
 
5
3
  module Reek
6
4
  module Smells
7
5
 
8
6
  class LongYieldList < LongParameterList
9
- def recognise?(args)
10
- @num_params = args.length
11
- Array === args and @num_params > MAX_ALLOWED
7
+
8
+ def self.contexts # :nodoc:
9
+ [:yield]
12
10
  end
13
11
 
14
- def detailed_report
15
- "#{@context} yields #{@num_params} parameters"
12
+ def initialize(config)
13
+ super
14
+ @action = 'yields'
16
15
  end
17
16
  end
18
-
19
17
  end
20
18
  end
@@ -1,6 +1,5 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
- require 'reek/smells/smell'
1
+ require 'reek/smells/smell_detector'
2
+ require 'reek/smell_warning'
4
3
 
5
4
  module Reek
6
5
  module Smells
@@ -10,15 +9,20 @@ module Reek
10
9
  #
11
10
  # +NestedIterators+ reports failing methods only once.
12
11
  #
13
- class NestedIterators < Smell
14
- def recognise?(already_in_iter)
15
- already_in_iter && @context
12
+ class NestedIterators < SmellDetector
13
+
14
+ def self.contexts # :nodoc:
15
+ [:iter]
16
16
  end
17
17
 
18
- def detailed_report
19
- "#{@context} has nested iterators"
18
+ #
19
+ # Checks whether the given +block+ is inside another.
20
+ # Any smells found are added to the +report+.
21
+ #
22
+ def examine_context(block, report)
23
+ return false unless block.nested_block?
24
+ report << SmellWarning.new(self, block, 'is nested')
20
25
  end
21
26
  end
22
-
23
27
  end
24
28
  end
@@ -0,0 +1,66 @@
1
+ class Class
2
+ def name_words
3
+ class_name = name.split(/::/)[-1]
4
+ class_name.gsub(/([a-z])([A-Z])/) { |sub| "#{$1} #{$2}"}.split
5
+ end
6
+ end
7
+
8
+ module Reek
9
+ module Smells
10
+
11
+ class SmellDetector
12
+
13
+ # The name of the config field that lists the names of code contexts
14
+ # that should not be checked. Add this field to the config for each
15
+ # smell that should ignore this code element.
16
+ EXCLUDE_KEY = 'exclude'
17
+
18
+ # The name fo the config field that specifies whether a smell is
19
+ # enabled. Set to +true+ or +false+.
20
+ ENABLED_KEY = 'enabled'
21
+
22
+ def self.class_name
23
+ self.name.split(/::/)[-1]
24
+ end
25
+
26
+ def self.contexts # :nodoc:
27
+ [:defn, :defs]
28
+ end
29
+
30
+ def self.default_config
31
+ {
32
+ ENABLED_KEY => true,
33
+ EXCLUDE_KEY => []
34
+ }
35
+ end
36
+
37
+ def self.listen(hooks, config)
38
+ detector = new(config[class_name])
39
+ contexts.each { |ctx| hooks[ctx] << detector }
40
+ end
41
+
42
+ def initialize(config)
43
+ @enabled = config[ENABLED_KEY]
44
+ @exceptions = config[EXCLUDE_KEY]
45
+ end
46
+
47
+ def examine(context, report)
48
+ before = report.size
49
+ examine_context(context, report) if @enabled and !exception?(context)
50
+ report.length > before
51
+ end
52
+
53
+ def examine_context(context, report)
54
+ end
55
+
56
+ def exception?(context)
57
+ return false if @exceptions.nil? or @exceptions.length == 0
58
+ context.matches?(@exceptions)
59
+ end
60
+
61
+ def smell_name
62
+ self.class.name_words.join(' ')
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,24 +1,85 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
1
  require 'reek/smells/control_couple'
4
2
  require 'reek/smells/duplication'
5
3
  require 'reek/smells/feature_envy'
4
+ require 'reek/smells/large_class'
6
5
  require 'reek/smells/long_method'
7
6
  require 'reek/smells/long_parameter_list'
8
7
  require 'reek/smells/long_yield_list'
9
8
  require 'reek/smells/nested_iterators'
10
9
  require 'reek/smells/uncommunicative_name'
11
10
  require 'reek/smells/utility_function'
11
+ require 'yaml'
12
12
 
13
- module Reek
13
+ class Hash
14
+ def value_merge!(other)
15
+ other.keys.each do |key|
16
+ self[key].adopt!(other[key])
17
+ end
18
+ self
19
+ end
14
20
 
15
- SMELLS = {
16
- :defn => [
17
- Smells::UncommunicativeName,
18
- Smells::LongMethod,
21
+ def adopt!(other)
22
+ other.keys.each do |key|
23
+ ov = other[key]
24
+ if Array === ov and has_key?(key)
25
+ self[key] += ov
26
+ else
27
+ self[key] = ov
28
+ end
29
+ end
30
+ self
31
+ end
32
+
33
+ def adopt(other)
34
+ self.deep_copy.adopt!(other)
35
+ end
36
+
37
+ def deep_copy
38
+ YAML::load(YAML::dump(self))
39
+ end
40
+ end
41
+
42
+ module Reek
43
+ class SmellConfig
44
+
45
+ SMELL_CLASSES = [
46
+ Smells::ControlCouple,
19
47
  Smells::Duplication,
48
+ Smells::FeatureEnvy,
49
+ Smells::LargeClass,
50
+ Smells::LongMethod,
51
+ Smells::LongParameterList,
52
+ Smells::LongYieldList,
53
+ Smells::NestedIterators,
54
+ Smells::UncommunicativeName,
20
55
  Smells::UtilityFunction,
21
- Smells::FeatureEnvy
22
- ]
23
- }
56
+ ]
57
+
58
+ def initialize
59
+ defaults_file = File.join(File.dirname(__FILE__), '..', '..', '..', 'config', 'defaults.reek')
60
+ @config = YAML.load_file(defaults_file)
61
+ end
62
+
63
+ def smell_listeners()
64
+ result = Hash.new {|hash,key| hash[key] = [] }
65
+ SMELL_CLASSES.each { |smell| smell.listen(result, @config) }
66
+ return result
67
+ end
68
+
69
+ def load_local(file)
70
+ path = File.expand_path(file)
71
+ all_reekfiles(path).each do |rfile|
72
+ cf = YAML.load_file(rfile)
73
+ @config.value_merge!(cf)
74
+ end
75
+ self
76
+ end
77
+
78
+ def all_reekfiles(path)
79
+ return [] unless File.exist?(path)
80
+ parent = File.dirname(path)
81
+ return [] if path == parent
82
+ all_reekfiles(parent) + Dir["#{path}/*.reek"]
83
+ end
84
+ end
24
85
  end
@@ -1,6 +1,5 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
- require 'reek/smells/smell'
1
+ require 'reek/smells/smell_detector'
2
+ require 'reek/smell_warning'
4
3
 
5
4
  module Reek
6
5
  module Smells
@@ -18,55 +17,64 @@ module Reek
18
17
  # * 1-character names
19
18
  # * names consisting of a single character followed by a number
20
19
  #
21
- class UncommunicativeName < Smell
20
+ class UncommunicativeName < SmellDetector
21
+
22
+ # The name of the config field that lists the regexps of
23
+ # smelly names to be rejected.
24
+ REJECT_KEY = 'reject'
25
+
26
+ # The name of the config field that lists the specific names that are
27
+ # to be treated as exceptions; these names will not be reported as
28
+ # uncommunicative.
29
+ ACCEPT_KEY = 'accept'
30
+
31
+ def self.default_config
32
+ super.adopt(
33
+ REJECT_KEY => [/^.[0-9]*$/],
34
+ ACCEPT_KEY => ['Inline::C']
35
+ )
36
+ end
37
+
38
+ def self.contexts # :nodoc:
39
+ [:module, :class, :defn, :defs, :iter]
40
+ end
41
+
42
+ def initialize(config = UncommunicativeName.default_config)
43
+ super
44
+ @reject = config[REJECT_KEY]
45
+ @accept = config[ACCEPT_KEY]
46
+ end
22
47
 
23
48
  #
24
- # Checks the given +method+ for uncommunicative method name,
25
- # parameter names, local variable names and instance variable names.
26
- # Any smells found are added to the +report+; returns true in that case,
27
- # and false otherwise.
49
+ # Checks the given +context+ for uncommunicative names.
50
+ # Any smells found are added to the +report+.
28
51
  #
29
- def self.examine(method, report)
30
- smell_reported = consider(method.name, method, report, 'method')
31
- method.parameters.each do |param|
32
- smell_reported = consider(param, method, report, 'parameter') || smell_reported
33
- end
34
- method.local_variables.each do |lvar|
35
- smell_reported = consider(lvar, method, report, 'local variable') || smell_reported
36
- end
37
- method.instance_variables.each do |ivar|
38
- smell_reported = consider(ivar, method, report, 'field') || smell_reported
39
- end
40
- smell_reported
52
+ def examine_context(context, report)
53
+ consider_name(context, report)
54
+ consider_variables(context, report)
41
55
  end
42
56
 
43
- def self.consider(sym, method, report, type) # :nodoc:
44
- name = sym.to_s
45
- if is_bad_name?(name)
46
- report << new(name, method, type)
47
- return true
57
+ def consider_variables(context, report) # :nodoc:
58
+ context.variable_names.each do |name|
59
+ next unless is_bad_name?(name)
60
+ report << SmellWarning.new(self, context,
61
+ "has the variable name '#{name}'")
48
62
  end
49
- return false
50
63
  end
51
64
 
52
- def self.is_bad_name?(name)
53
- return false if name == '*'
54
- name = name[1..-1] while /^@/ === name
55
- return true if name.length < 2
56
- return true if /^.[0-9]$/ === name
57
- false
65
+ def consider_name(context, report) # :nodoc:
66
+ name = context.name
67
+ return false if @accept.include?(context.to_s) # TODO: fq_name() ?
68
+ return false unless is_bad_name?(name)
69
+ report << SmellWarning.new(self, context,
70
+ "has the name '#{name}'")
58
71
  end
59
72
 
60
- def initialize(name, context, symbol_type)
61
- super(context, symbol_type)
62
- @bad_name = name
63
- @symbol_type = symbol_type
64
- end
65
-
66
- def detailed_report
67
- "#{@context} uses the #{@symbol_type} name '#{@bad_name}'"
73
+ def is_bad_name?(name) # :nodoc:
74
+ var = name.effective_name
75
+ return false if var == '*' or @accept.include?(var)
76
+ @reject.detect {|patt| patt === var}
68
77
  end
69
78
  end
70
-
71
79
  end
72
80
  end