transpec 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rubocop.yml +13 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +9 -0
  6. data/Guardfile +14 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +37 -0
  9. data/Rakefile +27 -0
  10. data/bin/transpec +8 -0
  11. data/lib/transpec/ast/scanner.rb +51 -0
  12. data/lib/transpec/ast/scope_stack.rb +76 -0
  13. data/lib/transpec/cli.rb +162 -0
  14. data/lib/transpec/configuration.rb +40 -0
  15. data/lib/transpec/git.rb +24 -0
  16. data/lib/transpec/rewriter.rb +109 -0
  17. data/lib/transpec/syntax/double.rb +21 -0
  18. data/lib/transpec/syntax/matcher.rb +60 -0
  19. data/lib/transpec/syntax/method_stub.rb +142 -0
  20. data/lib/transpec/syntax/send_node_syntax.rb +39 -0
  21. data/lib/transpec/syntax/should.rb +49 -0
  22. data/lib/transpec/syntax/should_receive.rb +120 -0
  23. data/lib/transpec/syntax.rb +58 -0
  24. data/lib/transpec/util.rb +50 -0
  25. data/lib/transpec/version.rb +14 -0
  26. data/lib/transpec.rb +17 -0
  27. data/spec/.rubocop.yml +19 -0
  28. data/spec/spec_helper.rb +33 -0
  29. data/spec/spec_spec.rb +54 -0
  30. data/spec/support/file_helper.rb +25 -0
  31. data/spec/support/shared_context.rb +63 -0
  32. data/spec/transpec/ast/scanner_spec.rb +177 -0
  33. data/spec/transpec/ast/scope_stack_spec.rb +94 -0
  34. data/spec/transpec/cli_spec.rb +290 -0
  35. data/spec/transpec/configuration_spec.rb +52 -0
  36. data/spec/transpec/git_spec.rb +85 -0
  37. data/spec/transpec/rewriter_spec.rb +203 -0
  38. data/spec/transpec/syntax/double_spec.rb +88 -0
  39. data/spec/transpec/syntax/matcher_spec.rb +407 -0
  40. data/spec/transpec/syntax/method_stub_spec.rb +386 -0
  41. data/spec/transpec/syntax/should_receive_spec.rb +286 -0
  42. data/spec/transpec/syntax/should_spec.rb +262 -0
  43. data/spec/transpec/util_spec.rb +48 -0
  44. data/transpec.gemspec +32 -0
  45. metadata +233 -0
@@ -0,0 +1,60 @@
1
+ # coding: utf-8
2
+
3
+ module Transpec
4
+ class Syntax
5
+ class Matcher < Syntax
6
+ include SendNodeSyntax, Util
7
+
8
+ def initialize(node, in_example_group_context, source_rewriter)
9
+ @node = node
10
+ @in_example_group_context = in_example_group_context
11
+ @source_rewriter = source_rewriter
12
+ end
13
+
14
+ def correct_operator!(parenthesize_arg = true)
15
+ case method_name
16
+ when :==
17
+ @source_rewriter.replace(selector_range, 'eq')
18
+ parenthesize!(parenthesize_arg)
19
+ when :===, :<, :<=, :>, :>=
20
+ @source_rewriter.insert_before(selector_range, 'be ')
21
+ when :=~
22
+ if arg_node.type == :array
23
+ @source_rewriter.replace(selector_range, 'match_array')
24
+ else
25
+ @source_rewriter.replace(selector_range, 'match')
26
+ end
27
+ parenthesize!(parenthesize_arg)
28
+ end
29
+ end
30
+
31
+ def parenthesize!(always = true)
32
+ return if here_document?(arg_node)
33
+
34
+ case left_parenthesis_range.source
35
+ when ' '
36
+ if always || arg_node.type == :hash
37
+ @source_rewriter.replace(left_parenthesis_range, '(')
38
+ @source_rewriter.insert_after(expression_range, ')')
39
+ end
40
+ when "\n", "\r"
41
+ @source_rewriter.insert_before(left_parenthesis_range, '(')
42
+ linefeed = left_parenthesis_range.source
43
+ matcher_line_indentation = indentation_of_line(@node)
44
+ right_parenthesis = "#{linefeed}#{matcher_line_indentation})"
45
+ @source_rewriter.insert_after(expression_range, right_parenthesis)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def left_parenthesis_range
52
+ Parser::Source::Range.new(
53
+ selector_range.source_buffer,
54
+ selector_range.end_pos,
55
+ selector_range.end_pos + 1
56
+ )
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,142 @@
1
+ # coding: utf-8
2
+
3
+ require 'English'
4
+
5
+ module Transpec
6
+ class Syntax
7
+ class MethodStub < Syntax
8
+ include SendNodeSyntax, Util
9
+
10
+ def self.target_node?(node)
11
+ return false unless node.type == :send
12
+ receiver_node, method_name, *_ = *node
13
+ return false unless receiver_node
14
+ [:stub, :unstub, :stub!, :unstub!].include?(method_name)
15
+ end
16
+
17
+ def allowize!
18
+ # There's no way of unstubbing in #allow syntax.
19
+ return unless [:stub, :stub!].include?(method_name)
20
+
21
+ if @replaced_deprecated_method
22
+ fail 'Already replaced deprecated method, cannot allowize.'
23
+ end
24
+
25
+ unless in_example_group_context?
26
+ fail NotInExampleGroupContextError.new(expression_range, "##{method_name}", '#allow')
27
+ end
28
+
29
+ if arg_node.type == :hash
30
+ expressions = build_allow_expressions_from_hash_node(arg_node)
31
+ @source_rewriter.replace(expression_range, expressions)
32
+ else
33
+ expression = build_allow_expression(arg_node)
34
+ @source_rewriter.replace(expression_range, expression)
35
+ end
36
+
37
+ @allowized = true
38
+ end
39
+
40
+ def replace_deprecated_method!
41
+ replacement_method_name = case method_name
42
+ when :stub! then 'stub'
43
+ when :unstub! then 'unstub'
44
+ end
45
+
46
+ return unless replacement_method_name
47
+
48
+ if @allowized
49
+ fail 'Already allowized, cannot replace deprecated method.'
50
+ end
51
+
52
+ @source_rewriter.replace(selector_range, replacement_method_name)
53
+
54
+ @replaced_deprecated_method = true
55
+ end
56
+
57
+ private
58
+
59
+ def build_allow_expressions_from_hash_node(hash_node)
60
+ expressions = []
61
+
62
+ hash_node.children.each_with_index do |pair_node, index|
63
+ key_node, value_node = *pair_node
64
+ expression = build_allow_expression(key_node, value_node, false)
65
+ expression.prepend(indentation_of_line(@node)) if index > 0
66
+ expressions << expression
67
+ end
68
+
69
+ expressions.join($RS)
70
+ end
71
+
72
+ def build_allow_expression(message_node, return_value_node = nil, keep_form_around_arg = true)
73
+ expression = ''
74
+
75
+ expression << if any_instance?
76
+ class_source = class_node_of_any_instance.loc.expression.source
77
+ "allow_any_instance_of(#{class_source})"
78
+ else
79
+ "allow(#{subject_range.source})"
80
+ end
81
+
82
+ expression << range_in_between_subject_and_selector.source
83
+ expression << 'to receive'
84
+ expression << (keep_form_around_arg ? range_in_between_selector_and_arg.source : '(')
85
+ expression << message_source(message_node)
86
+ expression << (keep_form_around_arg ? range_after_arg.source : ')')
87
+
88
+ if return_value_node
89
+ return_value_source = return_value_node.loc.expression.source
90
+ expression << ".and_return(#{return_value_source})"
91
+ end
92
+
93
+ expression
94
+ end
95
+
96
+ def class_node_of_any_instance
97
+ return nil unless subject_node.type == :send
98
+ return nil unless subject_node.children.count == 2
99
+ receiver_node, method_name = *subject_node
100
+ return nil unless method_name == :any_instance
101
+ return nil unless receiver_node.type == :const
102
+ receiver_node
103
+ end
104
+
105
+ def any_instance?
106
+ !class_node_of_any_instance.nil?
107
+ end
108
+
109
+ def message_source(node)
110
+ message_source = node.loc.expression.source
111
+ if node.type == :sym && !message_source.start_with?(':')
112
+ message_source.prepend(':')
113
+ end
114
+ message_source
115
+ end
116
+
117
+ def range_in_between_subject_and_selector
118
+ Parser::Source::Range.new(
119
+ subject_range.source_buffer,
120
+ subject_range.end_pos,
121
+ selector_range.begin_pos
122
+ )
123
+ end
124
+
125
+ def range_in_between_selector_and_arg
126
+ Parser::Source::Range.new(
127
+ selector_range.source_buffer,
128
+ selector_range.end_pos,
129
+ arg_range.begin_pos
130
+ )
131
+ end
132
+
133
+ def range_after_arg
134
+ Parser::Source::Range.new(
135
+ arg_range.source_buffer,
136
+ arg_range.end_pos,
137
+ expression_range.end_pos
138
+ )
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,39 @@
1
+ # coding: utf-8
2
+
3
+ module Transpec
4
+ class Syntax
5
+ module SendNodeSyntax
6
+ def receiver_node
7
+ @node.children[0]
8
+ end
9
+
10
+ alias_method :subject_node, :receiver_node
11
+
12
+ def method_name
13
+ @node.children[1]
14
+ end
15
+
16
+ def arg_node
17
+ @node.children[2]
18
+ end
19
+
20
+ def expression_range
21
+ @node.loc.expression
22
+ end
23
+
24
+ def selector_range
25
+ @node.loc.selector
26
+ end
27
+
28
+ def receiver_range
29
+ receiver_node.loc.expression
30
+ end
31
+
32
+ alias_method :subject_range, :receiver_range
33
+
34
+ def arg_range
35
+ arg_node.loc.expression
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ # coding: utf-8
2
+
3
+ module Transpec
4
+ class Syntax
5
+ class Should < Syntax
6
+ include SendNodeSyntax, Util
7
+
8
+ def self.target_node?(node)
9
+ return false unless node.type == :send
10
+ receiver_node, method_name, *_ = *node
11
+ return false unless receiver_node
12
+ [:should, :should_not].include?(method_name)
13
+ end
14
+
15
+ def positive?
16
+ method_name == :should
17
+ end
18
+
19
+ def expectize!(negative_form = 'not_to', parenthesize_matcher_arg = true)
20
+ unless in_example_group_context?
21
+ fail NotInExampleGroupContextError.new(expression_range, "##{method_name}", '#expect')
22
+ end
23
+
24
+ if proc_literal?(subject_node)
25
+ send_node = subject_node.children.first
26
+ range_of_subject_method_taking_block = send_node.loc.expression
27
+ @source_rewriter.replace(range_of_subject_method_taking_block, 'expect')
28
+ elsif subject_range.source[0] == '('
29
+ @source_rewriter.insert_before(subject_range, 'expect')
30
+ else
31
+ @source_rewriter.insert_before(subject_range, 'expect(')
32
+ @source_rewriter.insert_after(subject_range, ')')
33
+ end
34
+
35
+ @source_rewriter.replace(selector_range, positive? ? 'to' : negative_form)
36
+
37
+ matcher.correct_operator!(parenthesize_matcher_arg)
38
+ end
39
+
40
+ def matcher
41
+ @matcher ||= Matcher.new(matcher_node, in_example_group_context?, @source_rewriter)
42
+ end
43
+
44
+ def matcher_node
45
+ arg_node || parent_node
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,120 @@
1
+ # coding: utf-8
2
+
3
+ module Transpec
4
+ class Syntax
5
+ class ShouldReceive < Syntax
6
+ include SendNodeSyntax
7
+
8
+ def self.target_node?(node)
9
+ return false unless node.type == :send
10
+ receiver_node, method_name, *_ = *node
11
+ return false unless receiver_node
12
+ [:should_receive, :should_not_receive].include?(method_name)
13
+ end
14
+
15
+ def positive?
16
+ method_name == :should_receive
17
+ end
18
+
19
+ def expectize!(negative_form = 'not_to')
20
+ unless in_example_group_context?
21
+ fail NotInExampleGroupContextError.new(expression_range, "##{method_name}", '#expect')
22
+ end
23
+
24
+ if any_instance?(subject_node)
25
+ @source_rewriter.insert_before(subject_range, 'expect_any_instance_of(')
26
+ map = subject_node.loc
27
+ dot_any_instance_range = map.dot.join(map.selector)
28
+ @source_rewriter.replace(dot_any_instance_range, ')')
29
+ else
30
+ @source_rewriter.insert_before(subject_range, 'expect(')
31
+ @source_rewriter.insert_after(subject_range, ')')
32
+ end
33
+
34
+ to_receive = "#{positive? ? 'to' : negative_form} receive"
35
+ @source_rewriter.replace(selector_range, to_receive)
36
+
37
+ correct_block_style!
38
+ end
39
+
40
+ def correct_block_style!
41
+ broken_block_nodes = [
42
+ block_node_taken_by_with_method_with_no_normal_args,
43
+ block_node_followed_by_message_expectation_method
44
+ ].compact.uniq
45
+
46
+ return if broken_block_nodes.empty?
47
+
48
+ broken_block_nodes.each do |block_node|
49
+ map = block_node.loc
50
+ next if map.begin.source == '{'
51
+ @source_rewriter.replace(map.begin, '{')
52
+ @source_rewriter.replace(map.end, '}')
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def any_instance?(node)
59
+ return false unless node.type == :send
60
+ return false unless node.children.count == 2
61
+ receiver_node, method_name = *node
62
+ return false unless method_name == :any_instance
63
+ receiver_node.type == :const
64
+ end
65
+
66
+ # subject.should_receive(:method_name).once.with do |block_arg|
67
+ # end
68
+ #
69
+ # (block
70
+ # (send
71
+ # (send
72
+ # (send
73
+ # (send nil :subject) :should_receive
74
+ # (sym :method_name)) :once) :with)
75
+ # (args
76
+ # (arg :block_arg)) nil)
77
+ def block_node_taken_by_with_method_with_no_normal_args
78
+ @ancestor_nodes.reverse.reduce(@node) do |child_node, parent_node|
79
+ return nil unless [:send, :block].include?(parent_node.type)
80
+ return nil unless parent_node.children.first == child_node
81
+
82
+ if parent_node.type == :block
83
+ return nil unless child_node.children[1] == :with
84
+ return nil if child_node.children[2]
85
+ return parent_node
86
+ end
87
+
88
+ parent_node
89
+ end
90
+
91
+ nil
92
+ end
93
+
94
+ # subject.should_receive(:method_name) do |block_arg|
95
+ # end.once
96
+ #
97
+ # (send
98
+ # (block
99
+ # (send
100
+ # (send nil :subject) :should_receive
101
+ # (sym :method_name))
102
+ # (args
103
+ # (arg :block_arg)) nil) :once)
104
+ def block_node_followed_by_message_expectation_method
105
+ @ancestor_nodes.reverse.reduce(@node) do |child_node, parent_node|
106
+ return nil unless [:send, :block].include?(parent_node.type)
107
+ return nil unless parent_node.children.first == child_node
108
+
109
+ if child_node.type == :block && parent_node.type == :send
110
+ return child_node
111
+ end
112
+
113
+ parent_node
114
+ end
115
+
116
+ nil
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,58 @@
1
+ # coding: utf-8
2
+
3
+ module Transpec
4
+ class Syntax
5
+ class NotInExampleGroupContextError < StandardError
6
+ attr_reader :message, :source_range
7
+
8
+ def initialize(source_range, original_syntax, target_syntax)
9
+ @source_range = source_range
10
+ @message = build_message(original_syntax, target_syntax)
11
+ end
12
+
13
+ def source_buffer
14
+ @source_range.source_buffer
15
+ end
16
+
17
+ private
18
+
19
+ def build_message(original_syntax, target_syntax)
20
+ "Cannot convert #{original_syntax} into #{target_syntax} " +
21
+ "since #{target_syntax} is not available in the context."
22
+ end
23
+ end
24
+
25
+ attr_reader :node, :ancestor_nodes, :in_example_group_context, :source_rewriter
26
+ alias_method :in_example_group_context?, :in_example_group_context
27
+
28
+ def self.all
29
+ @subclasses ||= []
30
+ end
31
+
32
+ def self.inherited(subclass)
33
+ all << subclass
34
+ end
35
+
36
+ def self.snake_case_name
37
+ @snake_cake_name ||= begin
38
+ class_name = name.split('::').last
39
+ class_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
40
+ end
41
+ end
42
+
43
+ def self.target_node?(node)
44
+ false
45
+ end
46
+
47
+ def initialize(node, ancestor_nodes, in_example_group_context, source_rewriter)
48
+ @node = node
49
+ @ancestor_nodes = ancestor_nodes
50
+ @in_example_group_context = in_example_group_context
51
+ @source_rewriter = source_rewriter
52
+ end
53
+
54
+ def parent_node
55
+ @ancestor_nodes.last
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,50 @@
1
+ # coding: utf-8
2
+
3
+ module Transpec
4
+ module Util
5
+ module_function
6
+
7
+ def proc_literal?(node)
8
+ return false unless node.type == :block
9
+
10
+ send_node = node.children.first
11
+ receiver_node, method_name, *_ = *send_node
12
+
13
+ if receiver_node.nil? || const_name(receiver_node) == 'Kernel'
14
+ [:lambda, :proc].include?(method_name)
15
+ elsif const_name(receiver_node) == 'Proc'
16
+ method_name == :new
17
+ else
18
+ false
19
+ end
20
+ end
21
+
22
+ def const_name(node)
23
+ return nil if node.nil? || node.type != :const
24
+
25
+ const_names = []
26
+ const_node = node
27
+
28
+ loop do
29
+ namespace_node, name = *const_node
30
+ const_names << name
31
+ break unless namespace_node
32
+ break if namespace_node.type == :cbase
33
+ const_node = namespace_node
34
+ end
35
+
36
+ const_names.reverse.join('::')
37
+ end
38
+
39
+ def here_document?(node)
40
+ return false unless [:str, :dstr].include?(node.type)
41
+ node.loc.begin.source.start_with?('<<')
42
+ end
43
+
44
+ def indentation_of_line(node)
45
+ line = node.loc.expression.source_line
46
+ /^(?<indentation>\s*)\S/ =~ line
47
+ indentation
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,14 @@
1
+ # coding: utf-8
2
+
3
+ module Transpec
4
+ # http://semver.org/
5
+ module Version
6
+ MAJOR = 0
7
+ MINOR = 0
8
+ PATCH = 1
9
+
10
+ def self.to_s
11
+ [MAJOR, MINOR, PATCH].join('.')
12
+ end
13
+ end
14
+ end
data/lib/transpec.rb ADDED
@@ -0,0 +1,17 @@
1
+ # coding: utf-8
2
+
3
+ require 'transpec/configuration'
4
+ require 'transpec/cli'
5
+ require 'transpec/git'
6
+ require 'transpec/rewriter'
7
+ require 'transpec/util'
8
+ require 'transpec/version'
9
+ require 'transpec/ast/scanner'
10
+ require 'transpec/ast/scope_stack'
11
+ require 'transpec/syntax'
12
+ require 'transpec/syntax/send_node_syntax'
13
+ require 'transpec/syntax/double'
14
+ require 'transpec/syntax/matcher'
15
+ require 'transpec/syntax/method_stub'
16
+ require 'transpec/syntax/should'
17
+ require 'transpec/syntax/should_receive'
data/spec/.rubocop.yml ADDED
@@ -0,0 +1,19 @@
1
+
2
+ inherit_from: ../.rubocop.yml
3
+
4
+ # Avoid warning "Possibly useless use of == in void context"
5
+ # for `should ==`
6
+ Void:
7
+ Enabled: false
8
+
9
+ # Lengthen for long descriptions.
10
+ LineLength:
11
+ Max: 110
12
+
13
+ # raise_error matcher always requires {} form block.
14
+ Blocks:
15
+ Enabled: false
16
+
17
+ # Sometimes block alignment does not suit RSpec syntax.
18
+ BlockAlignment:
19
+ Enabled: false
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+
3
+ RSpec.configure do |config|
4
+ config.expect_with :rspec do |c|
5
+ # Yes, I'm writing specs in should syntax intentionally!
6
+ c.syntax = :should
7
+ end
8
+
9
+ config.treat_symbols_as_metadata_keys_with_true_values = true
10
+ config.filter_run_excluding do_not_run_in_transpeced_spec: ENV['TRANSPECED_SPEC']
11
+ end
12
+
13
+ Dir[File.join(File.dirname(__FILE__), 'support', '*')].each do |path|
14
+ require path
15
+ end
16
+
17
+ require 'simplecov'
18
+ SimpleCov.coverage_dir(File.join('spec', 'coverage'))
19
+
20
+ if ENV['TRAVIS']
21
+ require 'coveralls'
22
+ SimpleCov.formatter = Coveralls::SimpleCov::Formatter
23
+ elsif ENV['CI']
24
+ require 'simplecov-rcov'
25
+ SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
26
+ end
27
+
28
+ SimpleCov.start do
29
+ add_filter '/spec/'
30
+ add_filter '/vendor/bundle/'
31
+ end
32
+
33
+ require 'transpec'
data/spec/spec_spec.rb ADDED
@@ -0,0 +1,54 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'tmpdir'
5
+ require 'English'
6
+
7
+ describe 'Transpec project spec', :do_not_run_in_transpeced_spec do
8
+ around do |example|
9
+ Dir.chdir(project_root) do
10
+ example.run
11
+ end
12
+ end
13
+
14
+ let(:project_root) do
15
+ File.expand_path('..', File.dirname(__FILE__))
16
+ end
17
+
18
+ let(:spec_dir) do
19
+ File.join(project_root, 'spec')
20
+ end
21
+
22
+ TRANSPECED_SPEC_DIR = File.join(Dir.mktmpdir, 'transpeced_spec')
23
+
24
+ def silent_system(*args)
25
+ original_env = ENV.to_hash
26
+
27
+ if args.first.is_a?(Hash)
28
+ custom_env = args.shift
29
+ ENV.update(custom_env)
30
+ end
31
+
32
+ command = args.shelljoin
33
+ `#{command}`
34
+
35
+ ENV.replace(original_env)
36
+ $CHILD_STATUS.success?
37
+ rescue
38
+ ENV.replace(original_env)
39
+ false
40
+ end
41
+
42
+ it 'can be converted by Transpec itself without error' do
43
+ FileUtils.cp_r(spec_dir, TRANSPECED_SPEC_DIR)
44
+ silent_system('./bin/transpec', '--force', TRANSPECED_SPEC_DIR).should be_true
45
+ end
46
+
47
+ describe 'converted spec' do
48
+ it 'passes all' do
49
+ pending 'Need to rewrite syntax configuration in RSpec.configure'
50
+ env = { 'TRANSPECED_SPEC' => 'true' }
51
+ silent_system(env, 'rspec', TRANSPECED_SPEC_DIR).should be_true
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ require 'fileutils'
4
+
5
+ module FileHelper
6
+ module_function
7
+
8
+ def create_file(file_path, content)
9
+ file_path = File.expand_path(file_path)
10
+
11
+ dir_path = File.dirname(file_path)
12
+ FileUtils.makedirs(dir_path) unless File.exists?(dir_path)
13
+
14
+ File.open(file_path, 'w') do |file|
15
+ case content
16
+ when String
17
+ file.puts content
18
+ when Array
19
+ file.puts content.join("\n")
20
+ else
21
+ fail 'Unsupported type!'
22
+ end
23
+ end
24
+ end
25
+ end