reek 0.3.1 → 1.0.0

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