reek 1.3.8 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +11 -0
  3. data/README.md +22 -14
  4. data/Rakefile +2 -15
  5. data/assets/html_output.html.erb +103 -0
  6. data/features/command_line_interface/options.feature +3 -0
  7. data/features/command_line_interface/smell_selection.feature +19 -0
  8. data/features/rake_task/rake_task.feature +1 -1
  9. data/features/reports/reports.feature +16 -0
  10. data/features/reports/yaml.feature +26 -23
  11. data/features/samples.feature +2 -1
  12. data/features/step_definitions/reek_steps.rb +15 -15
  13. data/features/support/env.rb +7 -9
  14. data/lib/reek/cli/application.rb +2 -4
  15. data/lib/reek/cli/command.rb +12 -0
  16. data/lib/reek/cli/help_command.rb +3 -6
  17. data/lib/reek/cli/options.rb +147 -0
  18. data/lib/reek/cli/reek_command.rb +18 -14
  19. data/lib/reek/cli/report/formatter.rb +56 -0
  20. data/lib/reek/cli/report/report.rb +106 -0
  21. data/lib/reek/cli/report/strategy.rb +63 -0
  22. data/lib/reek/cli/version_command.rb +3 -6
  23. data/lib/reek/config_file_exception.rb +0 -1
  24. data/lib/reek/core/code_context.rb +1 -3
  25. data/lib/reek/core/code_parser.rb +13 -12
  26. data/lib/reek/core/method_context.rb +13 -2
  27. data/lib/reek/core/module_context.rb +0 -4
  28. data/lib/reek/core/object_refs.rb +2 -3
  29. data/lib/reek/core/singleton_method_context.rb +0 -2
  30. data/lib/reek/core/smell_configuration.rb +3 -5
  31. data/lib/reek/core/smell_repository.rb +7 -8
  32. data/lib/reek/core/sniffer.rb +4 -10
  33. data/lib/reek/core/stop_context.rb +2 -4
  34. data/lib/reek/core/warning_collector.rb +0 -1
  35. data/lib/reek/examiner.rb +19 -17
  36. data/lib/reek/rake/task.rb +7 -10
  37. data/lib/reek/smell_warning.rb +4 -8
  38. data/lib/reek/smells.rb +0 -1
  39. data/lib/reek/smells/attribute.rb +8 -11
  40. data/lib/reek/smells/boolean_parameter.rb +5 -7
  41. data/lib/reek/smells/class_variable.rb +6 -7
  42. data/lib/reek/smells/control_parameter.rb +78 -45
  43. data/lib/reek/smells/data_clump.rb +13 -16
  44. data/lib/reek/smells/duplicate_method_call.rb +13 -11
  45. data/lib/reek/smells/feature_envy.rb +6 -7
  46. data/lib/reek/smells/irresponsible_module.rb +4 -6
  47. data/lib/reek/smells/long_parameter_list.rb +5 -7
  48. data/lib/reek/smells/long_yield_list.rb +2 -4
  49. data/lib/reek/smells/nested_iterators.rb +12 -22
  50. data/lib/reek/smells/nil_check.rb +35 -46
  51. data/lib/reek/smells/prima_donna_method.rb +24 -16
  52. data/lib/reek/smells/repeated_conditional.rb +8 -10
  53. data/lib/reek/smells/smell_detector.rb +9 -7
  54. data/lib/reek/smells/too_many_instance_variables.rb +7 -9
  55. data/lib/reek/smells/too_many_methods.rb +6 -8
  56. data/lib/reek/smells/too_many_statements.rb +4 -6
  57. data/lib/reek/smells/uncommunicative_method_name.rb +5 -7
  58. data/lib/reek/smells/uncommunicative_module_name.rb +5 -7
  59. data/lib/reek/smells/uncommunicative_parameter_name.rb +7 -9
  60. data/lib/reek/smells/uncommunicative_variable_name.rb +15 -18
  61. data/lib/reek/smells/unused_parameters.rb +5 -45
  62. data/lib/reek/smells/utility_function.rb +9 -10
  63. data/lib/reek/source.rb +0 -1
  64. data/lib/reek/source/code_comment.rb +7 -8
  65. data/lib/reek/source/config_file.rb +2 -4
  66. data/lib/reek/source/core_extras.rb +1 -1
  67. data/lib/reek/source/reference_collector.rb +1 -2
  68. data/lib/reek/source/sexp_extensions.rb +93 -10
  69. data/lib/reek/source/sexp_formatter.rb +2 -3
  70. data/lib/reek/source/sexp_node.rb +19 -15
  71. data/lib/reek/source/source_code.rb +4 -14
  72. data/lib/reek/source/source_file.rb +3 -5
  73. data/lib/reek/source/source_locator.rb +5 -6
  74. data/lib/reek/source/source_repository.rb +3 -3
  75. data/lib/reek/source/tree_dresser.rb +2 -2
  76. data/lib/reek/spec.rb +1 -2
  77. data/lib/reek/spec/should_reek.rb +8 -5
  78. data/lib/reek/spec/should_reek_of.rb +6 -4
  79. data/lib/reek/spec/should_reek_only_of.rb +10 -6
  80. data/lib/reek/version.rb +1 -1
  81. data/reek.gemspec +34 -30
  82. data/spec/gem/updates_spec.rb +3 -4
  83. data/spec/gem/yard_spec.rb +1 -2
  84. data/spec/matchers/smell_of_matcher.rb +12 -14
  85. data/spec/quality/reek_source_spec.rb +42 -0
  86. data/spec/reek/cli/help_command_spec.rb +7 -5
  87. data/spec/reek/cli/report_spec.rb +89 -22
  88. data/spec/reek/cli/version_command_spec.rb +8 -6
  89. data/spec/reek/core/code_context_spec.rb +25 -26
  90. data/spec/reek/core/code_parser_spec.rb +6 -6
  91. data/spec/reek/core/method_context_spec.rb +18 -18
  92. data/spec/reek/core/module_context_spec.rb +5 -5
  93. data/spec/reek/core/object_refs_spec.rb +21 -22
  94. data/spec/reek/core/smell_configuration_spec.rb +22 -21
  95. data/spec/reek/core/stop_context_spec.rb +2 -2
  96. data/spec/reek/core/warning_collector_spec.rb +3 -3
  97. data/spec/reek/examiner_spec.rb +9 -9
  98. data/spec/reek/smell_warning_spec.rb +29 -29
  99. data/spec/reek/smells/attribute_spec.rb +6 -6
  100. data/spec/reek/smells/behaves_like_variable_detector.rb +6 -6
  101. data/spec/reek/smells/boolean_parameter_spec.rb +17 -17
  102. data/spec/reek/smells/class_variable_spec.rb +9 -9
  103. data/spec/reek/smells/control_parameter_spec.rb +161 -137
  104. data/spec/reek/smells/data_clump_spec.rb +22 -19
  105. data/spec/reek/smells/duplicate_method_call_spec.rb +71 -27
  106. data/spec/reek/smells/feature_envy_spec.rb +32 -32
  107. data/spec/reek/smells/irresponsible_module_spec.rb +21 -21
  108. data/spec/reek/smells/long_parameter_list_spec.rb +14 -14
  109. data/spec/reek/smells/long_yield_list_spec.rb +6 -6
  110. data/spec/reek/smells/nested_iterators_spec.rb +21 -21
  111. data/spec/reek/smells/nil_check_spec.rb +23 -15
  112. data/spec/reek/smells/prima_donna_method_spec.rb +5 -5
  113. data/spec/reek/smells/repeated_conditional_spec.rb +14 -14
  114. data/spec/reek/smells/smell_detector_shared.rb +9 -9
  115. data/spec/reek/smells/too_many_instance_variables_spec.rb +12 -12
  116. data/spec/reek/smells/too_many_methods_spec.rb +10 -10
  117. data/spec/reek/smells/too_many_statements_spec.rb +41 -41
  118. data/spec/reek/smells/uncommunicative_method_name_spec.rb +4 -4
  119. data/spec/reek/smells/uncommunicative_module_name_spec.rb +12 -12
  120. data/spec/reek/smells/uncommunicative_parameter_name_spec.rb +21 -21
  121. data/spec/reek/smells/uncommunicative_variable_name_spec.rb +49 -49
  122. data/spec/reek/smells/unused_parameters_spec.rb +26 -16
  123. data/spec/reek/smells/utility_function_spec.rb +20 -20
  124. data/spec/reek/source/code_comment_spec.rb +37 -37
  125. data/spec/reek/source/object_source_spec.rb +5 -5
  126. data/spec/reek/source/reference_collector_spec.rb +9 -9
  127. data/spec/reek/source/sexp_extensions_spec.rb +73 -52
  128. data/spec/reek/source/sexp_formatter_spec.rb +3 -4
  129. data/spec/reek/source/sexp_node_spec.rb +3 -3
  130. data/spec/reek/source/source_code_spec.rb +16 -15
  131. data/spec/reek/source/tree_dresser_spec.rb +2 -2
  132. data/spec/reek/spec/should_reek_of_spec.rb +11 -11
  133. data/spec/reek/spec/should_reek_only_of_spec.rb +11 -11
  134. data/spec/reek/spec/should_reek_spec.rb +11 -11
  135. data/spec/samples/one_smelly_file/dirty.rb +3 -0
  136. data/spec/spec_helper.rb +0 -6
  137. data/tasks/develop.rake +8 -16
  138. data/tasks/reek.rake +5 -13
  139. data/tasks/test.rake +5 -22
  140. metadata +56 -34
  141. data/lib/reek/cli/command_line.rb +0 -126
  142. data/lib/reek/cli/report.rb +0 -138
@@ -1,17 +1,15 @@
1
- $:.unshift 'lib'
2
-
3
1
  require 'rubygems'
4
2
  require 'tempfile'
5
3
  require 'fileutils'
4
+ require 'open3'
6
5
  require 'reek/cli/application'
7
6
 
8
7
  class ReekWorld
9
8
  def run(cmd)
10
- stderr_file = Tempfile.new('reek-world')
11
- stderr_file.close
12
- @last_stdout = `#{cmd} 2> #{stderr_file.path}`
13
- @last_exit_status = $?.exitstatus
14
- @last_stderr = IO.read(stderr_file.path)
9
+ out, err, status = Open3.capture3(cmd)
10
+ @last_stdout = out
11
+ @last_stderr = err
12
+ @last_exit_status = status.exitstatus
15
13
  end
16
14
 
17
15
  def reek(args)
@@ -31,9 +29,9 @@ EOS
31
29
  rakefile = Tempfile.new('rake_task', '.')
32
30
  rakefile.puts(header + task_def)
33
31
  rakefile.close
34
- run("RUBYOPT=rubygems rake -f #{rakefile.path} #{name}")
32
+ run("rake -f #{rakefile.path} #{name}")
35
33
  lines = @last_stdout.split("\n")
36
- if lines.length > 0 and lines[0] =~ /^\(/
34
+ if lines.length > 0 && lines[0] =~ /^\(/
37
35
  @last_stdout = lines[1..-1].join("\n")
38
36
  end
39
37
  end
@@ -1,15 +1,13 @@
1
- require 'reek/cli/command_line'
1
+ require 'reek/cli/options'
2
2
 
3
3
  module Reek
4
4
  module Cli
5
-
6
5
  #
7
6
  # Represents an instance of a Reek application.
8
7
  # This is the entry point for all invocations of Reek from the
9
8
  # command line.
10
9
  #
11
10
  class Application
12
-
13
11
  STATUS_SUCCESS = 0
14
12
  STATUS_ERROR = 1
15
13
  STATUS_SMELLS = 2
@@ -27,7 +25,7 @@ module Reek
27
25
  $stderr.puts "Error: #{error}"
28
26
  @status = STATUS_ERROR
29
27
  end
30
- return @status
28
+ @status
31
29
  end
32
30
 
33
31
  def output(text)
@@ -0,0 +1,12 @@
1
+ module Reek
2
+ module Cli
3
+ #
4
+ # Base class for all commands
5
+ #
6
+ class Command
7
+ def initialize(parser)
8
+ @parser = parser
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,16 +1,13 @@
1
+ require 'reek/cli/command'
1
2
 
2
3
  module Reek
3
4
  module Cli
4
-
5
5
  #
6
6
  # A command to display usage information for this application.
7
7
  #
8
- class HelpCommand
9
- def initialize(parser)
10
- @parser = parser
11
- end
8
+ class HelpCommand < Command
12
9
  def execute(view)
13
- view.output(@parser.to_s)
10
+ view.output(@parser.help_text)
14
11
  view.report_success
15
12
  end
16
13
  end
@@ -0,0 +1,147 @@
1
+ require 'optparse'
2
+ require 'rainbow'
3
+ require 'reek/cli/report/report'
4
+ require 'reek/cli/report/formatter'
5
+ require 'reek/cli/report/strategy'
6
+ require 'reek/cli/reek_command'
7
+ require 'reek/cli/help_command'
8
+ require 'reek/cli/version_command'
9
+ require 'reek/source'
10
+
11
+ module Reek
12
+ module Cli
13
+ #
14
+ # Parses the command line
15
+ #
16
+ class Options
17
+ def initialize(argv)
18
+ @argv = argv
19
+ @parser = OptionParser.new
20
+ @colored = true
21
+ @report_class = Report::TextReport
22
+ @strategy = Report::Strategy::Quiet
23
+ @warning_formatter = Report::WarningFormatterWithLineNumbers
24
+ @command_class = ReekCommand
25
+ @config_files = []
26
+ @sort_by_issue_count = false
27
+ @smells_to_detect = []
28
+ set_options
29
+ end
30
+
31
+ def banner
32
+ progname = @parser.program_name
33
+ # SMELL:
34
+ # The following banner isn't really correct. Help, Version and Reek
35
+ # are really sub-commands (in the git/svn sense) and so the usage
36
+ # banner should show three different command-lines. The other
37
+ # options are all flags for the Reek sub-command.
38
+ #
39
+ # reek -h|--help Display a help message
40
+ #
41
+ # reek -v|--version Output the tool's version number
42
+ #
43
+ # reek [options] files List the smells in the given files
44
+ # -c|--config file Specify file(s) with config options
45
+ # -n|--line-number Prefix smelly lines with line numbers
46
+ # -q|-[no-]quiet Only list files that have smells
47
+ # files Names of files or dirs to be checked
48
+ #
49
+ <<EOB
50
+ Usage: #{progname} [options] [files]
51
+
52
+ Examples:
53
+
54
+ #{progname} lib/*.rb
55
+ #{progname} -q lib
56
+ cat my_class.rb | #{progname}
57
+
58
+ See http://wiki.github.com/troessner/reek for detailed help.
59
+
60
+ EOB
61
+ end
62
+
63
+ def set_options
64
+ @parser.banner = banner
65
+ @parser.separator 'Common options:'
66
+ @parser.on('-h', '--help', 'Show this message') do
67
+ @command_class = HelpCommand
68
+ end
69
+ @parser.on('-v', '--version', 'Show version') do
70
+ @command_class = VersionCommand
71
+ end
72
+
73
+ @parser.separator "\nConfiguration:"
74
+ @parser.on('-c', '--config FILE', 'Read configuration options from FILE') do |file|
75
+ @config_files << file
76
+ end
77
+ @parser.on('--smell SMELL', 'Detect smell SMELL (default is all enabled smells)') do |smell|
78
+ @smells_to_detect << smell
79
+ end
80
+
81
+ @parser.separator "\nReport formatting:"
82
+ @parser.on('-o', '--[no-]color', 'Use colors for the output (this is the default)') do |opt|
83
+ @colored = opt
84
+ end
85
+ @parser.on('-q', '--quiet', 'Suppress headings for smell-free source files (this is the default)') do |_opt|
86
+ @strategy = Report::Strategy::Quiet
87
+ end
88
+ @parser.on('-V', '--no-quiet', '--verbose', 'Show headings for smell-free source files') do |_opt|
89
+ @strategy = Report::Strategy::Verbose
90
+ end
91
+ @parser.on('-U', '--ultra-verbose', 'Be as explanatory as possible') do |_opt|
92
+ @warning_formatter = Report::UltraVerboseWarningFormattter
93
+ end
94
+ @parser.on('-n', '--no-line-numbers', 'Suppress line numbers from the output') do
95
+ @warning_formatter = Report::SimpleWarningFormatter
96
+ end
97
+ @parser.on('--line-numbers', 'Show line numbers in the output (this is the default)') do
98
+ @warning_formatter = Report::WarningFormatterWithLineNumbers
99
+ end
100
+ @parser.on('-s', '--single-line', 'Show IDE-compatible single-line-per-warning') do
101
+ @warning_formatter = Report::SingleLineWarningFormatter
102
+ end
103
+ @parser.on('-S', '--sort-by-issue-count', 'Sort by "issue-count", listing the "smelliest" files first') do
104
+ @sort_by_issue_count = true
105
+ end
106
+ @parser.on('-y', '--yaml', 'Report smells in YAML format') do
107
+ @report_class = Report::YamlReport
108
+ end
109
+ @parser.on('-H', '--html', 'Report smells in HTML format') do
110
+ @report_class = Report::HtmlReport
111
+ end
112
+ end
113
+
114
+ def parse
115
+ @parser.parse!(@argv)
116
+ Rainbow.enabled = @colored
117
+ @command_class.new(self)
118
+ end
119
+
120
+ attr_reader :config_files
121
+ attr_reader :smells_to_detect
122
+
123
+ def reporter
124
+ @reporter ||= @report_class.new(warning_formatter: @warning_formatter,
125
+ report_formatter: Report::Formatter,
126
+ sort_by_issue_count: @sort_by_issue_count,
127
+ strategy: @strategy)
128
+ end
129
+
130
+ def sources
131
+ if @argv.empty?
132
+ return [$stdin.to_reek_source('$stdin')]
133
+ else
134
+ return Source::SourceLocator.new(@argv).all_sources
135
+ end
136
+ end
137
+
138
+ def program_name
139
+ @parser.program_name
140
+ end
141
+
142
+ def help_text
143
+ @parser.to_s
144
+ end
145
+ end
146
+ end
147
+ end
@@ -1,29 +1,33 @@
1
+ require 'reek/cli/command'
1
2
  require 'reek/examiner'
2
3
 
3
4
  module Reek
4
5
  module Cli
5
-
6
6
  #
7
7
  # A command to collect smells from a set of sources and write them out in
8
8
  # text report format.
9
9
  #
10
- class ReekCommand
11
- def self.create(sources, reporter, config_files = [])
12
- new(reporter, sources, config_files)
10
+ class ReekCommand < Command
11
+ def execute(app)
12
+ @parser.sources.each do |source|
13
+ reporter.add_examiner(Examiner.new(source, config_files, smell_names))
14
+ end
15
+ reporter.smells? ? app.report_smells : app.report_success
16
+ reporter.show
17
+ end
18
+
19
+ private
20
+
21
+ def reporter
22
+ @reporter ||= @parser.reporter
13
23
  end
14
24
 
15
- def initialize(reporter, sources, config_files = [])
16
- @sources = sources
17
- @reporter = reporter
18
- @config_files = config_files
25
+ def smell_names
26
+ @smell_names ||= @parser.smells_to_detect
19
27
  end
20
28
 
21
- def execute(app)
22
- @sources.each do |source|
23
- @reporter.add_examiner(Examiner.new(source, @config_files))
24
- end
25
- @reporter.has_smells? ? app.report_smells : app.report_success
26
- @reporter.show
29
+ def config_files
30
+ @config_files ||= @parser.config_files
27
31
  end
28
32
  end
29
33
  end
@@ -0,0 +1,56 @@
1
+ module Reek
2
+ module Cli
3
+ module Report
4
+ module Formatter
5
+ def self.format_list(warnings, formatter = SimpleWarningFormatter)
6
+ warnings.map do |warning|
7
+ " #{formatter.format warning}"
8
+ end.join("\n")
9
+ end
10
+
11
+ def self.header(examiner)
12
+ count = examiner.smells_count
13
+ result = Rainbow("#{examiner.description} -- ").cyan + Rainbow("#{count} warning").yellow
14
+ result += Rainbow('s').yellow unless count == 1
15
+ result
16
+ end
17
+ end
18
+
19
+ module UltraVerboseWarningFormattter
20
+ BASE_URL_FOR_HELP_LINK = 'https://github.com/troessner/reek/wiki/'
21
+
22
+ module_function
23
+
24
+ def format(warning)
25
+ "#{WarningFormatterWithLineNumbers.format(warning)} [#{explanatory_link(warning)}]"
26
+ end
27
+
28
+ def explanatory_link(warning)
29
+ "#{BASE_URL_FOR_HELP_LINK}#{class_name_to_param(warning.subclass)}"
30
+ end
31
+
32
+ def class_name_to_param(name)
33
+ name.split(/(?=[A-Z])/).join('-')
34
+ end
35
+ end
36
+
37
+ module SimpleWarningFormatter
38
+ def self.format(warning)
39
+ "#{warning.context} #{warning.message} (#{warning.subclass})"
40
+ end
41
+ end
42
+
43
+ module WarningFormatterWithLineNumbers
44
+ def self.format(warning)
45
+ "#{warning.lines.inspect}:#{SimpleWarningFormatter.format(warning)}"
46
+ end
47
+ end
48
+
49
+ module SingleLineWarningFormatter
50
+ def self.format(warning)
51
+ "#{warning.source}:#{warning.lines.first}: #{SimpleWarningFormatter.format(warning)}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,106 @@
1
+ require 'rainbow'
2
+
3
+ module Reek
4
+ module Cli
5
+ module Report
6
+ #
7
+ # A report that contains the smells and smell counts following source code analysis.
8
+ #
9
+ class Base
10
+ DEFAULT_FORMAT = :text
11
+ NO_WARNINGS_COLOR = :green
12
+ WARNINGS_COLOR = :red
13
+
14
+ def initialize(options = {})
15
+ @warning_formatter = options.fetch :warning_formatter, SimpleWarningFormatter
16
+ @report_formatter = options.fetch :report_formatter, Formatter
17
+ @examiners = []
18
+ @total_smell_count = 0
19
+ @sort_by_issue_count = options.fetch :sort_by_issue_count, false
20
+ @strategy = options.fetch(:strategy, Strategy::Quiet)
21
+ end
22
+
23
+ def add_examiner(examiner)
24
+ @total_smell_count += examiner.smells_count
25
+ @examiners << examiner
26
+ self
27
+ end
28
+
29
+ def smells?
30
+ @total_smell_count > 0
31
+ end
32
+
33
+ def smells
34
+ @strategy.new(@report_formatter, @warning_formatter, @examiners).gather_results
35
+ end
36
+ end
37
+
38
+ #
39
+ # Generates a sorted, text summary of smells in examiners
40
+ #
41
+ class TextReport < Base
42
+ def show
43
+ sort_examiners if smells?
44
+ display_summary
45
+ display_total_smell_count
46
+ end
47
+
48
+ private
49
+
50
+ def display_summary
51
+ print smells.reject(&:empty?).join("\n")
52
+ end
53
+
54
+ def display_total_smell_count
55
+ return unless @examiners.size > 1
56
+ print "\n"
57
+ print total_smell_count_message
58
+ end
59
+
60
+ def sort_examiners
61
+ @examiners.sort! { |first, second| second.smells_count <=> first.smells_count } if @sort_by_issue_count
62
+ end
63
+
64
+ def total_smell_count_message
65
+ colour = smells? ? WARNINGS_COLOR : NO_WARNINGS_COLOR
66
+ Rainbow("#{@total_smell_count} total warning#{'s' unless @total_smell_count == 1 }\n").color(colour)
67
+ end
68
+ end
69
+
70
+ #
71
+ # Displays a list of smells in YAML format
72
+ # YAML with empty array for 0 smells
73
+ class YamlReport < Base
74
+ def initialize(options = {})
75
+ @options = options
76
+ super options.merge!(strategy: Strategy::Normal)
77
+ end
78
+
79
+ def show
80
+ print(smells.to_yaml)
81
+ end
82
+ end
83
+
84
+ #
85
+ # Saves the report as a HTML file
86
+ #
87
+ class HtmlReport < Base
88
+ def initialize(options = {})
89
+ @options = options
90
+ super @options.merge!(strategy: Strategy::Normal)
91
+ end
92
+
93
+ require 'erb'
94
+
95
+ TEMPLATE = File.read(File.expand_path('../../../../../assets/html_output.html.erb', __FILE__))
96
+
97
+ def show
98
+ File.open('reek.html', 'w+') do |file|
99
+ file.puts ERB.new(TEMPLATE).result(binding)
100
+ end
101
+ print("Html file saved\n")
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end