kevinrutherford-reek 1.1.3.9 → 1.1.3.10

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