kevinrutherford-reek 1.1.3.9 → 1.1.3.10

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 (53) hide show
  1. data/History.txt +2 -1
  2. data/License.txt +20 -0
  3. data/bin/reek +5 -5
  4. data/features/options.feature +1 -0
  5. data/features/reports.feature +40 -0
  6. data/features/stdin.feature +10 -1
  7. data/features/step_definitions/reek_steps.rb +2 -2
  8. data/features/support/env.rb +2 -2
  9. data/lib/reek/class_context.rb +2 -2
  10. data/lib/reek/code_parser.rb +4 -0
  11. data/lib/reek/core_extras.rb +50 -0
  12. data/lib/reek/object_source.rb +7 -18
  13. data/lib/reek/options.rb +13 -3
  14. data/lib/reek/report.rb +27 -28
  15. data/lib/reek/sexp_formatter.rb +2 -0
  16. data/lib/reek/smells/control_couple.rb +5 -0
  17. data/lib/reek/sniffer.rb +98 -3
  18. data/lib/reek/source.rb +9 -112
  19. data/lib/reek/spec.rb +29 -55
  20. data/lib/reek.rb +1 -1
  21. data/reek.gemspec +4 -4
  22. data/spec/reek/object_source_spec.rb +3 -3
  23. data/spec/reek/report_spec.rb +9 -5
  24. data/spec/reek/should_reek_of_spec.rb +105 -0
  25. data/spec/reek/should_reek_only_of_spec.rb +85 -0
  26. data/spec/reek/{spec_spec.rb → should_reek_spec.rb} +24 -3
  27. data/spec/reek/smells/duplication_spec.rb +1 -1
  28. data/spec/reek/smells/large_class_spec.rb +1 -0
  29. data/spec/reek/smells/long_method_spec.rb +4 -4
  30. data/spec/reek/smells/long_parameter_list_spec.rb +1 -1
  31. data/spec/reek/smells/smell_detector_spec.rb +1 -1
  32. data/spec/reek/smells/uncommunicative_name_spec.rb +2 -1
  33. data/spec/reek/sniffer_spec.rb +10 -0
  34. data/spec/samples/all_but_one_masked/clean_one.rb +6 -0
  35. data/spec/samples/all_but_one_masked/dirty.rb +7 -0
  36. data/spec/samples/all_but_one_masked/masked.reek +5 -0
  37. data/spec/samples/clean_due_to_masking/clean_one.rb +6 -0
  38. data/spec/samples/clean_due_to_masking/clean_three.rb +6 -0
  39. data/spec/samples/clean_due_to_masking/clean_two.rb +6 -0
  40. data/spec/samples/clean_due_to_masking/dirty_one.rb +7 -0
  41. data/spec/samples/clean_due_to_masking/dirty_two.rb +7 -0
  42. data/spec/samples/clean_due_to_masking/masked.reek +7 -0
  43. data/spec/samples/mixed_results/clean_one.rb +6 -0
  44. data/spec/samples/mixed_results/clean_three.rb +6 -0
  45. data/spec/samples/mixed_results/clean_two.rb +6 -0
  46. data/spec/samples/mixed_results/dirty_one.rb +7 -0
  47. data/spec/samples/mixed_results/dirty_two.rb +7 -0
  48. data/spec/slow/inline_spec.rb +6 -2
  49. data/spec/slow/optparse_spec.rb +6 -2
  50. data/spec/slow/redcloth_spec.rb +6 -2
  51. data/tasks/test.rake +2 -0
  52. metadata +23 -4
  53. data/spec/slow/source_list_spec.rb +0 -40
data/History.txt CHANGED
@@ -1,17 +1,18 @@
1
1
  == 1.2 (in progress -- see github)
2
2
 
3
3
  === Major Changes
4
- * Reek's RDoc is now hosted at http://rdoc.info/projects/kevinrutherford/reek
5
4
  * Reek's output reports are now formatted differently:
6
5
  ** Reek is no longer silent about smell-free source code
7
6
  ** Output now reports on all files examined, even if they have no smells
8
7
  ** Smell warnings are indented in the report; file summary headers are not
9
8
  ** Reports for multiple sources are run together; no more blank lines
9
+ ** Reports in spec matcher failures are quiet
10
10
  * The smells masked by *.reek config files can now be seen:
11
11
  ** The header for each source file now counts masked smells
12
12
  ** The --show-all (-a) option shows masked warnings in the report
13
13
 
14
14
  === Minor Changes
15
+ * Reek's RDoc is now hosted at http://rdoc.info/projects/kevinrutherford/reek
15
16
  * Several changes to the LongMethod counting algorithm:
16
17
  ** LongMethod now counts statements deeper into each method (fixed #25)
17
18
  ** LongMethod no longer counts control structures, only their contained stmts
data/License.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008,2009 Kevin Rutherford
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/reek CHANGED
@@ -6,15 +6,15 @@
6
6
  # Author: Kevin Rutherford
7
7
  #
8
8
 
9
- require 'reek'
10
- require 'reek/source'
11
9
  require 'reek/options'
12
10
 
13
11
  def reek(args)
14
12
  begin
15
- source = Reek::Options.parse(args)
16
- puts source.full_report
17
- return source.smelly? ? 2 : 0
13
+ sniffer = Reek::Options.parse(args)
14
+ # SMELL:
15
+ # This should use the actual type of report selected by the user's options
16
+ puts sniffer.full_report
17
+ return sniffer.smelly? ? 2 : 0
18
18
  rescue SystemExit => ex
19
19
  return ex.status
20
20
  rescue Exception => error
@@ -32,6 +32,7 @@ Feature: Reek can be controlled using command-line options
32
32
 
33
33
  Options:
34
34
  -a, --[no-]show-all Show all smells, including those masked by config settings
35
+ -q, --quiet Suppress headings for smell-free source files
35
36
  -h, --help Show this message
36
37
  -f, --format FORMAT Specify the format of smell warnings
37
38
  -c, --context-first Sort by context; sets the format string to "%m%c %w (%s)"
@@ -37,4 +37,44 @@ Feature: Correctly formatted reports
37
37
 
38
38
  """
39
39
 
40
+ Scenario Outline: --quiet turns off headers for fragrant files
41
+ When I run reek <option> spec/samples/three_clean_files/*.rb
42
+ Then it succeeds
43
+ And it reports:
44
+ """
45
+
46
+
47
+ """
48
+
49
+ Examples:
50
+ | option |
51
+ | -q |
52
+ | --quiet |
53
+
54
+
55
+ Scenario Outline: -a turns on details in presence of -q
56
+ When I run reek <options> spec/samples/clean_due_to_masking/*.rb
57
+ Then it succeeds
58
+ And it reports:
59
+ """
60
+ spec/samples/clean_due_to_masking/dirty_one.rb -- 0 warnings (+6 masked):
61
+ (masked) Dirty has the variable name '@s' (Uncommunicative Name)
62
+ (masked) Dirty#a calls @s.title multiple times (Duplication)
63
+ (masked) Dirty#a calls puts(@s.title) multiple times (Duplication)
64
+ (masked) Dirty#a has the name 'a' (Uncommunicative Name)
65
+ (masked) Dirty#a/block has the variable name 'x' (Uncommunicative Name)
66
+ (masked) Dirty#a/block/block is nested (Nested Iterators)
67
+ spec/samples/clean_due_to_masking/dirty_two.rb -- 0 warnings (+6 masked):
68
+ (masked) Dirty has the variable name '@s' (Uncommunicative Name)
69
+ (masked) Dirty#a calls @s.title multiple times (Duplication)
70
+ (masked) Dirty#a calls puts(@s.title) multiple times (Duplication)
71
+ (masked) Dirty#a has the name 'a' (Uncommunicative Name)
72
+ (masked) Dirty#a/block has the variable name 'x' (Uncommunicative Name)
73
+ (masked) Dirty#a/block/block is nested (Nested Iterators)
74
+
75
+ """
40
76
 
77
+ Examples:
78
+ | options |
79
+ | -q -a |
80
+ | -a -q |
@@ -13,13 +13,22 @@ Feature: Reek reads from $stdin when no files are given
13
13
 
14
14
  """
15
15
 
16
- Scenario: outputs nothing on empty stdin
16
+ Scenario: outputs header only on empty stdin
17
17
  When I pass "" to reek
18
18
  Then it succeeds
19
19
  And it reports:
20
20
  """
21
21
  $stdin -- 0 warnings
22
22
 
23
+ """
24
+
25
+ Scenario: outputs nothing on empty stdin in quiet mode
26
+ When I pass "" to reek --quiet
27
+ Then it succeeds
28
+ And it reports:
29
+ """
30
+
31
+
23
32
  """
24
33
 
25
34
  Scenario: return non-zero status when there are smells
@@ -2,8 +2,8 @@ When /^I run reek (.*)$/ do |args|
2
2
  run args
3
3
  end
4
4
 
5
- When /^I pass "([^\"]*)" to reek$/ do |stdin|
6
- run_with_pipe stdin
5
+ When /^I pass "([^\"]*)" to reek *(.*)$/ do |stdin, args|
6
+ run_with_pipe(stdin, args)
7
7
  end
8
8
 
9
9
  When /^I run rake reek$/ do
@@ -16,10 +16,10 @@ class CucumberWorld
16
16
  @last_stderr = IO.read(stderr_file.path)
17
17
  end
18
18
 
19
- def run_with_pipe(stdin)
19
+ def run_with_pipe(stdin, args)
20
20
  stderr_file = Tempfile.new('cucumber')
21
21
  stderr_file.close
22
- @last_stdout = `echo \"#{stdin}\" | ruby -Ilib bin/reek 2> #{stderr_file.path}`
22
+ @last_stdout = `echo \"#{stdin}\" | ruby -Ilib bin/reek #{args} 2> #{stderr_file.path}`
23
23
  @last_exit_status = $?.exitstatus
24
24
  @last_stderr = IO.read(stderr_file.path)
25
25
  end
@@ -16,8 +16,8 @@ module Reek
16
16
  end
17
17
 
18
18
  def ClassContext.from_s(src)
19
- source = Source.from_s(src)
20
- CodeParser.new(Sniffer.new).process_class(source.generate_syntax_tree)
19
+ sniffer = src.sniff
20
+ CodeParser.new(sniffer).process_class(sniffer.source.syntax_tree)
21
21
  end
22
22
 
23
23
  def initialize(outer, name, superclass = nil)
@@ -9,6 +9,10 @@ require 'reek/method_context'
9
9
  require 'reek/singleton_method_context'
10
10
  require 'reek/yield_call_context'
11
11
 
12
+ #
13
+ # Extensions to +Sexp+ to allow +CodeParser+ to navigate the abstract
14
+ # syntax tree more easily.
15
+ #
12
16
  class Sexp
13
17
  def children
14
18
  find_all { |item| Sexp === item }
@@ -0,0 +1,50 @@
1
+ require 'reek/source' # SMELL: should refer to Sniffer
2
+ require 'reek/sniffer'
3
+
4
+ class File
5
+ #
6
+ # Creates a new +Sniffer+ that assumes this File contains Ruby source
7
+ # code and examines that code for smells.
8
+ #
9
+ def sniff
10
+ result = Reek::Sniffer.new
11
+ Reek::Source.from_path(self.path, result)
12
+ result
13
+ end
14
+ end
15
+
16
+ class IO
17
+ #
18
+ # Creates a new +Sniffer+ that assumes this IO stream contains Ruby source
19
+ # code and examines that code for smells.
20
+ #
21
+ def sniff(description = 'io')
22
+ code = self.readlines.join
23
+ result = Reek::Sniffer.new
24
+ Reek::Source.new(code, description, result)
25
+ result
26
+ end
27
+ end
28
+
29
+ class String
30
+ #
31
+ # Creates a new +Sniffer+ that assumes this String contains Ruby source
32
+ # code and examines that code for smells.
33
+ #
34
+ def sniff
35
+ result = Reek::Sniffer.new
36
+ Reek::Source.new(self, 'string', result)
37
+ result
38
+ end
39
+ end
40
+
41
+ class Array
42
+ #
43
+ # Creates a new +Sniffer+ that assumes this Array contains the names
44
+ # of Ruby source files and examines those files for smells.
45
+ #
46
+ def sniff
47
+ sniffers = self.map {|path| File.new(path).sniff }
48
+ Reek::SnifferSet.new(sniffers, 'dir')
49
+ end
50
+ end
@@ -1,15 +1,4 @@
1
1
  module Reek
2
- class Source
3
- #
4
- # Factory method: creates a +Source+ from obj.
5
- # The code is not parsed until +report+ is called.
6
- # (This feature is only enabled if you have the ParseTree gem installed.)
7
- #
8
- def self.from_object(obj)
9
- return ObjectSource.new(obj, obj.to_s)
10
- end
11
- end
12
-
13
2
  class ObjectSource < Source # :nodoc:
14
3
 
15
4
  def self.unify(sexp) # :nodoc:
@@ -20,7 +9,7 @@ module Reek
20
9
  return unifier.process(sexp[0])
21
10
  end
22
11
 
23
- def initialize(code, desc) # :nodoc:
12
+ def initialize(code, desc, sniffer) # :nodoc:
24
13
  super
25
14
  @sniffer.disable(LargeClass)
26
15
  end
@@ -35,7 +24,7 @@ module Reek
35
24
  end
36
25
  end
37
26
 
38
- def generate_syntax_tree
27
+ def syntax_tree
39
28
  if can_parse_objects?
40
29
  ObjectSource.unify(ParseTree.new.parse_tree(@source))
41
30
  else
@@ -47,12 +36,12 @@ end
47
36
 
48
37
  class Object
49
38
  #
50
- # Constructs a Source representing this object; the source can then be used
51
- # to generate an abstract syntax tree for the object, which can in turn then
52
- # be examined for code smells.
39
+ # Constructs a Sniffer which examines this object for code smells.
53
40
  # (This feature is only enabled if you have the ParseTree gem installed.)
54
41
  #
55
- def to_source
56
- Reek::Source.from_object(self)
42
+ def sniff
43
+ result = Sniffer.new
44
+ ObjectSource.new(self, self.to_s, result)
45
+ result
57
46
  end
58
47
  end
data/lib/reek/options.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'optparse'
2
+ require 'reek'
2
3
  require 'reek/source'
4
+ require 'reek/core_extras' # SMELL
3
5
 
4
6
  module Reek
5
7
 
@@ -11,7 +13,8 @@ module Reek
11
13
  def self.default_options
12
14
  {
13
15
  :format => CTX_SORT,
14
- :show_all => false
16
+ :show_all => false,
17
+ :quiet => false
15
18
  }
16
19
  end
17
20
 
@@ -40,12 +43,16 @@ EOB
40
43
  set_all_options(opts, config)
41
44
  end
42
45
 
46
+ # SMELL: Greedy Module
47
+ # This creates the command-line parser AND invokes it. And for the
48
+ # -v and -h options it also executes them. And it holds the config
49
+ # options for the rest of the application.
43
50
  def self.parse(args)
44
51
  @@opts = parse_args(args)
45
52
  if args.length > 0
46
- return Source.from_pathlist(args)
53
+ return args.sniff
47
54
  else
48
- return Source.from_io($stdin, '$stdin')
55
+ return $stdin.sniff('$stdin')
49
56
  end
50
57
  end
51
58
 
@@ -69,6 +76,9 @@ EOB
69
76
  opts.on("-a", "--[no-]show-all", "Show all smells, including those masked by config settings") do |opt|
70
77
  config[:show_all] = opt
71
78
  end
79
+ opts.on("-q", "--quiet", "Suppress headings for smell-free source files") do
80
+ config[:quiet] = true
81
+ end
72
82
  end
73
83
 
74
84
  def self.set_help_option(opts)
data/lib/reek/report.rb CHANGED
@@ -1,22 +1,15 @@
1
1
  require 'set'
2
2
  require 'reek/sniffer'
3
- require 'reek/smells/smell_detector'
4
3
 
5
4
  module Reek
6
5
  class Report
7
6
  include Enumerable
8
7
 
9
- def initialize(sniffer = nil) # :nodoc:
8
+ def initialize(sniffer) # :nodoc:
10
9
  @masked_warnings = SortedSet.new
11
10
  @warnings = SortedSet.new
12
- sniffer.report_on(self) if sniffer
13
- end
14
-
15
- #
16
- # Yields, in turn, each SmellWarning in this report.
17
- #
18
- def each
19
- @warnings.each { |smell| yield smell }
11
+ @desc = sniffer.desc
12
+ sniffer.report_on(self)
20
13
  end
21
14
 
22
15
  #
@@ -52,19 +45,25 @@ module Reek
52
45
 
53
46
  # Creates a formatted report of all the +Smells::SmellWarning+ objects recorded in
54
47
  # this report, with a heading.
55
- def full_report(desc)
56
- result = header(desc, @warnings.length)
57
- result += ":\n#{to_s}" if should_report
48
+ def full_report
49
+ return quiet_report if Options[:quiet]
50
+ result = header(@warnings.length)
51
+ result += ":\n#{smell_list}" if should_report
58
52
  result += "\n"
59
53
  result
60
54
  end
61
55
 
56
+ def quiet_report
57
+ return '' unless should_report
58
+ "#{header(@warnings.length)}:\n#{smell_list}\n"
59
+ end
60
+
62
61
  def should_report
63
62
  @warnings.length > 0 or (Options[:show_all] and @masked_warnings.length > 0)
64
63
  end
65
64
 
66
- def header(desc, num_smells)
67
- result = "#{desc} -- #{num_smells} warning"
65
+ def header(num_smells)
66
+ result = "#{@desc} -- #{num_smells} warning"
68
67
  result += 's' unless num_smells == 1
69
68
  result += " (+#{@masked_warnings.length} masked)" unless @masked_warnings.empty?
70
69
  result
@@ -72,7 +71,7 @@ module Reek
72
71
 
73
72
  # Creates a formatted report of all the +Smells::SmellWarning+ objects recorded in
74
73
  # this report.
75
- def to_s
74
+ def smell_list
76
75
  all = SortedSet.new(@warnings)
77
76
  all.merge(@masked_warnings) if Options[:show_all]
78
77
  all.map {|smell| " #{smell.report}"}.join("\n")
@@ -82,15 +81,8 @@ module Reek
82
81
  class ReportList
83
82
  include Enumerable
84
83
 
85
- def initialize(sources)
86
- @sources = sources
87
- end
88
-
89
- #
90
- # Yields, in turn, each SmellWarning in every report in this report.
91
- #
92
- def each(&blk)
93
- @sources.each {|src| src.report.each(&blk) }
84
+ def initialize(sniffers)
85
+ @sniffers = sniffers
94
86
  end
95
87
 
96
88
  def empty?
@@ -98,11 +90,18 @@ module Reek
98
90
  end
99
91
 
100
92
  def length
101
- @sources.inject(0) {|sum, src| sum + src.report.length }
93
+ @sniffers.inject(0) {|sum, sniffer| sum + sniffer.num_smells }
102
94
  end
103
95
 
96
+ # SMELL: Shotgun Surgery
97
+ # This method and the next will have to be repeated for every new
98
+ # kind of report.
104
99
  def full_report
105
- @sources.map { |src| src.full_report }.join
100
+ @sniffers.map { |sniffer| sniffer.full_report }.join
101
+ end
102
+
103
+ def quiet_report
104
+ @sniffers.map { |sniffer| sniffer.quiet_report }.join
106
105
  end
107
106
 
108
107
  #
@@ -110,7 +109,7 @@ module Reek
110
109
  # only if one of them has a report string matching all of the +patterns+.
111
110
  #
112
111
  def has_smell?(smell_class, patterns)
113
- @sources.any? { |smell| smell.has_smell?(smell_class, patterns) }
112
+ @sniffers.any? { |sniffer| sniffer.has_smell?(smell_class, patterns) }
114
113
  end
115
114
  end
116
115
  end
@@ -1,3 +1,5 @@
1
+ require 'rubygems'
2
+ require 'ruby_parser'
1
3
  require 'ruby2ruby'
2
4
 
3
5
  module Reek
@@ -53,6 +53,11 @@ module Reek
53
53
  #
54
54
  def examine_context(cond)
55
55
  return unless cond.tests_a_parameter?
56
+ # SMELL: Duplication
57
+ # This smell is reported once for each conditional that tests the
58
+ # same parameter. Which means that the same smell can recur within
59
+ # a single sniffer. Which in turn means that the sniffer can't count
60
+ # its smells without knowing which are duplicates.
56
61
  found(cond, "is controlled by argument #{SexpFormatter.format(cond.if_expr)}")
57
62
  end
58
63
  end
data/lib/reek/sniffer.rb CHANGED
@@ -10,6 +10,8 @@ require 'reek/smells/nested_iterators'
10
10
  require 'reek/smells/uncommunicative_name'
11
11
  require 'reek/smells/utility_function'
12
12
  require 'reek/config_file'
13
+ require 'reek/code_parser'
14
+ require 'reek/report'
13
15
  require 'yaml'
14
16
 
15
17
  class Hash
@@ -41,6 +43,8 @@ end
41
43
  module Reek
42
44
  class Sniffer
43
45
 
46
+ # SMELL: Duplication
47
+ # This list should be calculated by looking in the source folder.
44
48
  SMELL_CLASSES = [
45
49
  Smells::ControlCouple,
46
50
  Smells::Duplication,
@@ -54,13 +58,12 @@ module Reek
54
58
  Smells::UtilityFunction,
55
59
  ]
56
60
 
61
+ attr_accessor :source
62
+
57
63
  def initialize
58
- defaults_file = File.join(File.dirname(__FILE__), '..', '..', 'config', 'defaults.reek')
59
- @config = YAML.load_file(defaults_file)
60
64
  @typed_detectors = nil
61
65
  @detectors = Hash.new
62
66
  SMELL_CLASSES.each { |klass| @detectors[klass] = DetectorStack.new(klass.new) }
63
- @listeners = []
64
67
  end
65
68
 
66
69
  #
@@ -93,6 +96,60 @@ module Reek
93
96
  listeners.each {|smell| smell.examine(scope) } if listeners
94
97
  end
95
98
 
99
+ #
100
+ # Returns a +Report+ listing the smells found in this source. The first
101
+ # call to +report+ parses the source code and constructs a list of
102
+ # +SmellWarning+s found; subsequent calls simply return this same list.
103
+ #
104
+ def report
105
+ unless @report
106
+ CodeParser.new(self).process(@source.syntax_tree)
107
+ @report = Report.new(self)
108
+ end
109
+ @report
110
+ end
111
+
112
+ def smelly?
113
+ report.length > 0
114
+ end
115
+
116
+ def quiet_report
117
+ report.quiet_report
118
+ end
119
+
120
+ # SMELL: Shotgun Surgery
121
+ # This and the above method will need to be replicated for every new
122
+ # kind of report.
123
+ def full_report
124
+ report.full_report
125
+ end
126
+
127
+ def num_smells
128
+ report.length
129
+ end
130
+
131
+ def desc
132
+ # SMELL: Special Case
133
+ # Only used in the Report tests, because they don't always create a Source.
134
+ @source ? @source.desc : "unknown"
135
+ end
136
+
137
+ #
138
+ # Checks for instances of +smell_class+, and returns +true+
139
+ # only if one of them has a report string matching all of the +patterns+.
140
+ #
141
+ def has_smell?(smell_class, patterns=[])
142
+ report.has_smell?(smell_class, patterns)
143
+ end
144
+
145
+ def smells_only_of?(klass, patterns)
146
+ report.length == 1 and has_smell?(klass, patterns)
147
+ end
148
+
149
+ def sniff
150
+ self
151
+ end
152
+
96
153
  private
97
154
 
98
155
  def smell_listeners()
@@ -110,4 +167,42 @@ private
110
167
  all_reekfiles(parent) + Dir["#{path}/*.reek"]
111
168
  end
112
169
  end
170
+
171
+ class SnifferSet
172
+
173
+ attr_reader :sniffers, :desc
174
+
175
+ def initialize(sniffers, desc)
176
+ @sniffers = sniffers
177
+ @desc = desc
178
+ end
179
+
180
+ def smelly?
181
+ @sniffers.any? {|sniffer| sniffer.smelly? }
182
+ end
183
+
184
+ #
185
+ # Checks for instances of +smell_class+, and returns +true+
186
+ # only if one of them has a report string matching all of the +patterns+.
187
+ #
188
+ def has_smell?(smell_class, patterns=[])
189
+ @sniffers.any? {|sniffer| sniffer.has_smell?(smell_class, patterns)}
190
+ end
191
+
192
+ def smells_only_of?(klass, patterns)
193
+ ReportList.new(@sniffers).length == 1 and has_smell?(klass, patterns)
194
+ end
195
+
196
+ def quiet_report
197
+ ReportList.new(@sniffers).quiet_report
198
+ end
199
+
200
+
201
+ # SMELL: Shotgun Surgery
202
+ # This and the above method will need to be replicated for every new
203
+ # kind of report.
204
+ def full_report
205
+ ReportList.new(@sniffers).full_report
206
+ end
207
+ end
113
208
  end