rubocop-rails 2.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +73 -0
- data/bin/setup +7 -0
- data/config/default.yml +466 -0
- data/lib/rubocop-rails.rb +12 -0
- data/lib/rubocop/cop/mixin/target_rails_version.rb +16 -0
- data/lib/rubocop/cop/rails/action_filter.rb +117 -0
- data/lib/rubocop/cop/rails/active_record_aliases.rb +48 -0
- data/lib/rubocop/cop/rails/active_record_override.rb +82 -0
- data/lib/rubocop/cop/rails/active_support_aliases.rb +69 -0
- data/lib/rubocop/cop/rails/application_job.rb +40 -0
- data/lib/rubocop/cop/rails/application_record.rb +40 -0
- data/lib/rubocop/cop/rails/assert_not.rb +44 -0
- data/lib/rubocop/cop/rails/belongs_to.rb +102 -0
- data/lib/rubocop/cop/rails/blank.rb +164 -0
- data/lib/rubocop/cop/rails/bulk_change_table.rb +289 -0
- data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +91 -0
- data/lib/rubocop/cop/rails/date.rb +161 -0
- data/lib/rubocop/cop/rails/delegate.rb +132 -0
- data/lib/rubocop/cop/rails/delegate_allow_blank.rb +37 -0
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +91 -0
- data/lib/rubocop/cop/rails/enum_uniqueness.rb +45 -0
- data/lib/rubocop/cop/rails/environment_comparison.rb +68 -0
- data/lib/rubocop/cop/rails/exit.rb +67 -0
- data/lib/rubocop/cop/rails/file_path.rb +108 -0
- data/lib/rubocop/cop/rails/find_by.rb +55 -0
- data/lib/rubocop/cop/rails/find_each.rb +51 -0
- data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +25 -0
- data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +106 -0
- data/lib/rubocop/cop/rails/helper_instance_variable.rb +39 -0
- data/lib/rubocop/cop/rails/http_positional_arguments.rb +117 -0
- data/lib/rubocop/cop/rails/http_status.rb +160 -0
- data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +94 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +246 -0
- data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +175 -0
- data/lib/rubocop/cop/rails/link_to_blank.rb +98 -0
- data/lib/rubocop/cop/rails/not_null_column.rb +67 -0
- data/lib/rubocop/cop/rails/output.rb +49 -0
- data/lib/rubocop/cop/rails/output_safety.rb +99 -0
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +107 -0
- data/lib/rubocop/cop/rails/presence.rb +124 -0
- data/lib/rubocop/cop/rails/present.rb +153 -0
- data/lib/rubocop/cop/rails/read_write_attribute.rb +74 -0
- data/lib/rubocop/cop/rails/redundant_allow_nil.rb +111 -0
- data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +136 -0
- data/lib/rubocop/cop/rails/reflection_class_name.rb +37 -0
- data/lib/rubocop/cop/rails/refute_methods.rb +76 -0
- data/lib/rubocop/cop/rails/relative_date_constant.rb +93 -0
- data/lib/rubocop/cop/rails/request_referer.rb +56 -0
- data/lib/rubocop/cop/rails/reversible_migration.rb +286 -0
- data/lib/rubocop/cop/rails/safe_navigation.rb +87 -0
- data/lib/rubocop/cop/rails/save_bang.rb +316 -0
- data/lib/rubocop/cop/rails/scope_args.rb +29 -0
- data/lib/rubocop/cop/rails/skips_model_validations.rb +87 -0
- data/lib/rubocop/cop/rails/time_zone.rb +238 -0
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +105 -0
- data/lib/rubocop/cop/rails/unknown_env.rb +63 -0
- data/lib/rubocop/cop/rails/validation.rb +109 -0
- data/lib/rubocop/cop/rails_cops.rb +64 -0
- data/lib/rubocop/rails.rb +12 -0
- data/lib/rubocop/rails/inject.rb +18 -0
- data/lib/rubocop/rails/version.rb +10 -0
- metadata +143 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for scope calls where it was passed
|
7
|
+
# a method (usually a scope) instead of a lambda/proc.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
#
|
11
|
+
# # bad
|
12
|
+
# scope :something, where(something: true)
|
13
|
+
#
|
14
|
+
# # good
|
15
|
+
# scope :something, -> { where(something: true) }
|
16
|
+
class ScopeArgs < Cop
|
17
|
+
MSG = 'Use `lambda`/`proc` instead of a plain method call.'
|
18
|
+
|
19
|
+
def_node_matcher :scope?, '(send nil? :scope _ $send)'
|
20
|
+
|
21
|
+
def on_send(node)
|
22
|
+
scope?(node) do |second_arg|
|
23
|
+
add_offense(second_arg)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for the use of methods which skip
|
7
|
+
# validations which are listed in
|
8
|
+
# https://guides.rubyonrails.org/active_record_validations.html#skipping-validations
|
9
|
+
#
|
10
|
+
# Methods may be ignored from this rule by configuring a `Whitelist`.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # bad
|
14
|
+
# Article.first.decrement!(:view_count)
|
15
|
+
# DiscussionBoard.decrement_counter(:post_count, 5)
|
16
|
+
# Article.first.increment!(:view_count)
|
17
|
+
# DiscussionBoard.increment_counter(:post_count, 5)
|
18
|
+
# person.toggle :active
|
19
|
+
# product.touch
|
20
|
+
# Billing.update_all("category = 'authorized', author = 'David'")
|
21
|
+
# user.update_attribute(:website, 'example.com')
|
22
|
+
# user.update_columns(last_request_at: Time.current)
|
23
|
+
# Post.update_counters 5, comment_count: -1, action_count: 1
|
24
|
+
#
|
25
|
+
# # good
|
26
|
+
# user.update(website: 'example.com')
|
27
|
+
# FileUtils.touch('file')
|
28
|
+
#
|
29
|
+
# @example Whitelist: ["touch"]
|
30
|
+
# # bad
|
31
|
+
# DiscussionBoard.decrement_counter(:post_count, 5)
|
32
|
+
# DiscussionBoard.increment_counter(:post_count, 5)
|
33
|
+
# person.toggle :active
|
34
|
+
#
|
35
|
+
# # good
|
36
|
+
# user.touch
|
37
|
+
#
|
38
|
+
class SkipsModelValidations < Cop
|
39
|
+
MSG = 'Avoid using `%<method>s` because it skips validations.'
|
40
|
+
|
41
|
+
METHODS_WITH_ARGUMENTS = %w[decrement!
|
42
|
+
decrement_counter
|
43
|
+
increment!
|
44
|
+
increment_counter
|
45
|
+
toggle!
|
46
|
+
update_all
|
47
|
+
update_attribute
|
48
|
+
update_column
|
49
|
+
update_columns
|
50
|
+
update_counters].freeze
|
51
|
+
|
52
|
+
def_node_matcher :good_touch?, <<-PATTERN
|
53
|
+
(send (const nil? :FileUtils) :touch ...)
|
54
|
+
PATTERN
|
55
|
+
|
56
|
+
def on_send(node)
|
57
|
+
return if whitelist.include?(node.method_name.to_s)
|
58
|
+
return unless blacklist.include?(node.method_name.to_s)
|
59
|
+
return if allowed_method?(node)
|
60
|
+
return if good_touch?(node)
|
61
|
+
|
62
|
+
add_offense(node, location: :selector)
|
63
|
+
end
|
64
|
+
alias on_csend on_send
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def message(node)
|
69
|
+
format(MSG, method: node.method_name)
|
70
|
+
end
|
71
|
+
|
72
|
+
def allowed_method?(node)
|
73
|
+
METHODS_WITH_ARGUMENTS.include?(node.method_name.to_s) &&
|
74
|
+
!node.arguments?
|
75
|
+
end
|
76
|
+
|
77
|
+
def blacklist
|
78
|
+
cop_config['Blacklist'] || []
|
79
|
+
end
|
80
|
+
|
81
|
+
def whitelist
|
82
|
+
cop_config['Whitelist'] || []
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for the use of Time methods without zone.
|
7
|
+
#
|
8
|
+
# Built on top of Ruby on Rails style guide (https://github.com/rubocop-hq/rails-style-guide#time)
|
9
|
+
# and the article http://danilenko.org/2012/7/6/rails_timezones/
|
10
|
+
#
|
11
|
+
# Two styles are supported for this cop. When EnforcedStyle is 'strict'
|
12
|
+
# then only use of Time.zone is allowed.
|
13
|
+
#
|
14
|
+
# When EnforcedStyle is 'flexible' then it's also allowed
|
15
|
+
# to use Time.in_time_zone.
|
16
|
+
#
|
17
|
+
# @example EnforcedStyle: strict
|
18
|
+
# # `strict` means that `Time` should be used with `zone`.
|
19
|
+
#
|
20
|
+
# # bad
|
21
|
+
# Time.now
|
22
|
+
# Time.parse('2015-03-02 19:05:37')
|
23
|
+
#
|
24
|
+
# # bad
|
25
|
+
# Time.current
|
26
|
+
# Time.at(timestamp).in_time_zone
|
27
|
+
#
|
28
|
+
# # good
|
29
|
+
# Time.zone.now
|
30
|
+
# Time.zone.parse('2015-03-02 19:05:37')
|
31
|
+
#
|
32
|
+
# @example EnforcedStyle: flexible (default)
|
33
|
+
# # `flexible` allows usage of `in_time_zone` instead of `zone`.
|
34
|
+
#
|
35
|
+
# # bad
|
36
|
+
# Time.now
|
37
|
+
# Time.parse('2015-03-02 19:05:37')
|
38
|
+
#
|
39
|
+
# # good
|
40
|
+
# Time.zone.now
|
41
|
+
# Time.zone.parse('2015-03-02 19:05:37')
|
42
|
+
#
|
43
|
+
# # good
|
44
|
+
# Time.current
|
45
|
+
# Time.at(timestamp).in_time_zone
|
46
|
+
class TimeZone < Cop
|
47
|
+
include ConfigurableEnforcedStyle
|
48
|
+
|
49
|
+
MSG = 'Do not use `%<current>s` without zone. Use `%<prefer>s` ' \
|
50
|
+
'instead.'
|
51
|
+
|
52
|
+
MSG_ACCEPTABLE = 'Do not use `%<current>s` without zone. ' \
|
53
|
+
'Use one of %<prefer>s instead.'
|
54
|
+
|
55
|
+
MSG_LOCALTIME = 'Do not use `Time.localtime` without ' \
|
56
|
+
'offset or zone.'
|
57
|
+
|
58
|
+
TIMECLASSES = %i[Time DateTime].freeze
|
59
|
+
|
60
|
+
GOOD_METHODS = %i[zone zone_default find_zone find_zone!].freeze
|
61
|
+
|
62
|
+
DANGEROUS_METHODS = %i[now local new parse at current].freeze
|
63
|
+
|
64
|
+
ACCEPTED_METHODS = %i[in_time_zone utc getlocal xmlschema iso8601
|
65
|
+
jisx0301 rfc3339 httpdate to_i to_f].freeze
|
66
|
+
|
67
|
+
def on_const(node)
|
68
|
+
mod, klass = *node
|
69
|
+
# we should only check core classes
|
70
|
+
# (`DateTime`, `Time`, `::DateTime` or `::Time`)
|
71
|
+
return unless (mod.nil? || mod.cbase_type?) && method_send?(node)
|
72
|
+
|
73
|
+
check_time_node(klass, node.parent) if TIMECLASSES.include?(klass)
|
74
|
+
end
|
75
|
+
|
76
|
+
def autocorrect(node)
|
77
|
+
lambda do |corrector|
|
78
|
+
# add `.zone`: `Time.at` => `Time.zone.at`
|
79
|
+
corrector.insert_after(node.children[0].source_range, '.zone')
|
80
|
+
# replace `Time.zone.current` => `Time.zone.now`
|
81
|
+
if node.method_name == :current
|
82
|
+
corrector.replace(node.loc.selector, 'now')
|
83
|
+
end
|
84
|
+
# prefer `Time` over `DateTime` class
|
85
|
+
if strict?
|
86
|
+
corrector.replace(node.children.first.source_range, 'Time')
|
87
|
+
end
|
88
|
+
remove_redundant_in_time_zone(corrector, node)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# remove redundant `.in_time_zone` from `Time.zone.now.in_time_zone`
|
95
|
+
def remove_redundant_in_time_zone(corrector, node)
|
96
|
+
time_methods_called = extract_method_chain(node)
|
97
|
+
return unless time_methods_called.include?(:in_time_zone) ||
|
98
|
+
time_methods_called.include?(:zone)
|
99
|
+
|
100
|
+
while node&.send_type?
|
101
|
+
if node.children.last == :in_time_zone
|
102
|
+
in_time_zone_with_dot =
|
103
|
+
node.loc.selector.adjust(begin_pos: -1)
|
104
|
+
corrector.remove(in_time_zone_with_dot)
|
105
|
+
end
|
106
|
+
node = node.parent
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def check_time_node(klass, node)
|
111
|
+
chain = extract_method_chain(node)
|
112
|
+
return if not_danger_chain?(chain)
|
113
|
+
|
114
|
+
return check_localtime(node) if need_check_localtime?(chain)
|
115
|
+
|
116
|
+
method_name = (chain & DANGEROUS_METHODS).join('.')
|
117
|
+
|
118
|
+
return if offset_provided?(node)
|
119
|
+
|
120
|
+
message = build_message(klass, method_name, node)
|
121
|
+
|
122
|
+
add_offense(node, location: :selector, message: message)
|
123
|
+
end
|
124
|
+
|
125
|
+
def build_message(klass, method_name, node)
|
126
|
+
if flexible?
|
127
|
+
format(
|
128
|
+
MSG_ACCEPTABLE,
|
129
|
+
current: "#{klass}.#{method_name}",
|
130
|
+
prefer: acceptable_methods(klass, method_name, node).join(', ')
|
131
|
+
)
|
132
|
+
else
|
133
|
+
safe_method_name = safe_method(method_name, node)
|
134
|
+
format(MSG,
|
135
|
+
current: "#{klass}.#{method_name}",
|
136
|
+
prefer: "Time.zone.#{safe_method_name}")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def extract_method_chain(node)
|
141
|
+
chain = []
|
142
|
+
while !node.nil? && node.send_type?
|
143
|
+
chain << node.method_name if method_from_time_class?(node)
|
144
|
+
node = node.parent
|
145
|
+
end
|
146
|
+
chain
|
147
|
+
end
|
148
|
+
|
149
|
+
# Only add the method to the chain if the method being
|
150
|
+
# called is part of the time class.
|
151
|
+
def method_from_time_class?(node)
|
152
|
+
receiver, method_name, *_args = *node
|
153
|
+
if (receiver.is_a? RuboCop::AST::Node) && !receiver.cbase_type?
|
154
|
+
method_from_time_class?(receiver)
|
155
|
+
else
|
156
|
+
TIMECLASSES.include?(method_name)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# checks that parent node of send_type
|
161
|
+
# and receiver is the given node
|
162
|
+
def method_send?(node)
|
163
|
+
return false unless node.parent&.send_type?
|
164
|
+
|
165
|
+
node.parent.receiver == node
|
166
|
+
end
|
167
|
+
|
168
|
+
def safe_method(method_name, node)
|
169
|
+
if %w[new current].include?(method_name)
|
170
|
+
node.arguments? ? 'local' : 'now'
|
171
|
+
else
|
172
|
+
method_name
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def check_localtime(node)
|
177
|
+
selector_node = node
|
178
|
+
|
179
|
+
while node&.send_type?
|
180
|
+
break if node.method_name == :localtime
|
181
|
+
|
182
|
+
node = node.parent
|
183
|
+
end
|
184
|
+
|
185
|
+
return if node.arguments?
|
186
|
+
|
187
|
+
add_offense(selector_node,
|
188
|
+
location: :selector, message: MSG_LOCALTIME)
|
189
|
+
end
|
190
|
+
|
191
|
+
def not_danger_chain?(chain)
|
192
|
+
(chain & DANGEROUS_METHODS).empty? || !(chain & good_methods).empty?
|
193
|
+
end
|
194
|
+
|
195
|
+
def need_check_localtime?(chain)
|
196
|
+
flexible? && chain.include?(:localtime)
|
197
|
+
end
|
198
|
+
|
199
|
+
def flexible?
|
200
|
+
style == :flexible
|
201
|
+
end
|
202
|
+
|
203
|
+
def strict?
|
204
|
+
style == :strict
|
205
|
+
end
|
206
|
+
|
207
|
+
def good_methods
|
208
|
+
if strict?
|
209
|
+
GOOD_METHODS
|
210
|
+
else
|
211
|
+
GOOD_METHODS + [:current] + ACCEPTED_METHODS
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def acceptable_methods(klass, method_name, node)
|
216
|
+
acceptable = [
|
217
|
+
"`Time.zone.#{safe_method(method_name, node)}`",
|
218
|
+
"`#{klass}.current`"
|
219
|
+
]
|
220
|
+
|
221
|
+
ACCEPTED_METHODS.each do |am|
|
222
|
+
acceptable << "`#{klass}.#{method_name}.#{am}`"
|
223
|
+
end
|
224
|
+
|
225
|
+
acceptable
|
226
|
+
end
|
227
|
+
|
228
|
+
# Time.new can be called with a time zone offset
|
229
|
+
# When it is, that should be considered safe
|
230
|
+
# Example:
|
231
|
+
# Time.new(1988, 3, 15, 3, 0, 0, "-05:00")
|
232
|
+
def offset_provided?(node)
|
233
|
+
node.arguments.size >= 7
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# Prefer the use of uniq (or distinct), before pluck instead of after.
|
7
|
+
#
|
8
|
+
# The use of uniq before pluck is preferred because it executes within
|
9
|
+
# the database.
|
10
|
+
#
|
11
|
+
# This cop has two different enforcement modes. When the EnforcedStyle
|
12
|
+
# is conservative (the default) then only calls to pluck on a constant
|
13
|
+
# (i.e. a model class) before uniq are added as offenses.
|
14
|
+
#
|
15
|
+
# When the EnforcedStyle is aggressive then all calls to pluck before
|
16
|
+
# uniq are added as offenses. This may lead to false positives as the cop
|
17
|
+
# cannot distinguish between calls to pluck on an ActiveRecord::Relation
|
18
|
+
# vs a call to pluck on an ActiveRecord::Associations::CollectionProxy.
|
19
|
+
#
|
20
|
+
# Autocorrect is disabled by default for this cop since it may generate
|
21
|
+
# false positives.
|
22
|
+
#
|
23
|
+
# @example EnforcedStyle: conservative (default)
|
24
|
+
# # bad
|
25
|
+
# Model.pluck(:id).uniq
|
26
|
+
#
|
27
|
+
# # good
|
28
|
+
# Model.uniq.pluck(:id)
|
29
|
+
#
|
30
|
+
# @example EnforcedStyle: aggressive
|
31
|
+
# # bad
|
32
|
+
# # this will return a Relation that pluck is called on
|
33
|
+
# Model.where(cond: true).pluck(:id).uniq
|
34
|
+
#
|
35
|
+
# # bad
|
36
|
+
# # an association on an instance will return a CollectionProxy
|
37
|
+
# instance.assoc.pluck(:id).uniq
|
38
|
+
#
|
39
|
+
# # bad
|
40
|
+
# Model.pluck(:id).uniq
|
41
|
+
#
|
42
|
+
# # good
|
43
|
+
# Model.uniq.pluck(:id)
|
44
|
+
#
|
45
|
+
class UniqBeforePluck < RuboCop::Cop::Cop
|
46
|
+
include ConfigurableEnforcedStyle
|
47
|
+
include RangeHelp
|
48
|
+
|
49
|
+
MSG = 'Use `%<method>s` before `pluck`.'
|
50
|
+
NEWLINE = "\n"
|
51
|
+
PATTERN = '[!^block (send (send %<type>s :pluck ...) ' \
|
52
|
+
'${:uniq :distinct} ...)]'
|
53
|
+
|
54
|
+
def_node_matcher :conservative_node_match,
|
55
|
+
format(PATTERN, type: 'const')
|
56
|
+
|
57
|
+
def_node_matcher :aggressive_node_match,
|
58
|
+
format(PATTERN, type: '_')
|
59
|
+
|
60
|
+
def on_send(node)
|
61
|
+
method = if style == :conservative
|
62
|
+
conservative_node_match(node)
|
63
|
+
else
|
64
|
+
aggressive_node_match(node)
|
65
|
+
end
|
66
|
+
|
67
|
+
return unless method
|
68
|
+
|
69
|
+
add_offense(node, location: :selector,
|
70
|
+
message: format(MSG, method: method))
|
71
|
+
end
|
72
|
+
|
73
|
+
def autocorrect(node)
|
74
|
+
lambda do |corrector|
|
75
|
+
method = node.method_name
|
76
|
+
|
77
|
+
corrector.remove(dot_method_with_whitespace(method, node))
|
78
|
+
corrector.insert_before(node.receiver.loc.dot.begin, ".#{method}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def style_parameter_name
|
85
|
+
'EnforcedStyle'
|
86
|
+
end
|
87
|
+
|
88
|
+
def dot_method_with_whitespace(method, node)
|
89
|
+
range_between(dot_method_begin_pos(method, node),
|
90
|
+
node.loc.selector.end_pos)
|
91
|
+
end
|
92
|
+
|
93
|
+
def dot_method_begin_pos(method, node)
|
94
|
+
lines = node.source.split(NEWLINE)
|
95
|
+
|
96
|
+
if lines.last.strip == ".#{method}"
|
97
|
+
node.source.rindex(NEWLINE)
|
98
|
+
else
|
99
|
+
node.loc.dot.begin_pos
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|