rubocop 0.6.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rubocop might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -266
- data/CHANGELOG.md +49 -7
- data/README.md +75 -2
- data/Rakefile +2 -2
- data/bin/rubocop +15 -10
- data/lib/rubocop.rb +19 -1
- data/lib/rubocop/cli.rb +113 -116
- data/lib/rubocop/config.rb +202 -0
- data/lib/rubocop/config_store.rb +37 -0
- data/lib/rubocop/cop/alias.rb +2 -5
- data/lib/rubocop/cop/align_parameters.rb +1 -1
- data/lib/rubocop/cop/array_literal.rb +43 -4
- data/lib/rubocop/cop/avoid_for.rb +2 -4
- data/lib/rubocop/cop/avoid_global_vars.rb +49 -0
- data/lib/rubocop/cop/block_comments.rb +17 -0
- data/lib/rubocop/cop/brace_after_percent.rb +9 -5
- data/lib/rubocop/cop/{indentation.rb → case_indentation.rb} +1 -1
- data/lib/rubocop/cop/class_methods.rb +20 -0
- data/lib/rubocop/cop/colon_method_call.rb +44 -0
- data/lib/rubocop/cop/cop.rb +30 -2
- data/lib/rubocop/cop/def_parentheses.rb +1 -1
- data/lib/rubocop/cop/empty_line_between_defs.rb +26 -0
- data/lib/rubocop/cop/empty_lines.rb +10 -13
- data/lib/rubocop/cop/eval.rb +22 -0
- data/lib/rubocop/cop/favor_join.rb +37 -0
- data/lib/rubocop/cop/grammar.rb +2 -2
- data/lib/rubocop/cop/hash_literal.rb +43 -4
- data/lib/rubocop/cop/hash_syntax.rb +2 -2
- data/lib/rubocop/cop/if_then_else.rb +1 -1
- data/lib/rubocop/cop/leading_comment_space.rb +20 -0
- data/lib/rubocop/cop/line_continuation.rb +18 -0
- data/lib/rubocop/cop/line_length.rb +1 -1
- data/lib/rubocop/cop/method_and_variable_snake_case.rb +7 -6
- data/lib/rubocop/cop/method_length.rb +4 -15
- data/lib/rubocop/cop/not.rb +15 -0
- data/lib/rubocop/cop/offence.rb +9 -0
- data/lib/rubocop/cop/semicolon.rb +74 -3
- data/lib/rubocop/cop/single_line_methods.rb +60 -0
- data/lib/rubocop/cop/space_after_control_keyword.rb +28 -0
- data/lib/rubocop/cop/surrounding_space.rb +48 -9
- data/lib/rubocop/cop/symbol_array.rb +29 -0
- data/lib/rubocop/cop/trivial_accessors.rb +103 -0
- data/lib/rubocop/cop/unless_else.rb +1 -1
- data/lib/rubocop/cop/variable_interpolation.rb +3 -2
- data/lib/rubocop/cop/word_array.rb +38 -0
- data/lib/rubocop/version.rb +1 -1
- data/rubocop.gemspec +11 -7
- data/spec/project_spec.rb +27 -0
- data/spec/rubocop/cli_spec.rb +549 -487
- data/spec/rubocop/config_spec.rb +399 -0
- data/spec/rubocop/config_store_spec.rb +66 -0
- data/spec/rubocop/cops/alias_spec.rb +7 -0
- data/spec/rubocop/cops/array_literal_spec.rb +8 -1
- data/spec/rubocop/cops/avoid_for_spec.rb +15 -1
- data/spec/rubocop/cops/avoid_global_vars.rb +32 -0
- data/spec/rubocop/cops/block_comments_spec.rb +29 -0
- data/spec/rubocop/cops/brace_after_percent_spec.rb +19 -13
- data/spec/rubocop/cops/{indentation_spec.rb → case_indentation_spec.rb} +2 -2
- data/spec/rubocop/cops/class_methods_spec.rb +49 -0
- data/spec/rubocop/cops/colon_method_call_spec.rb +47 -0
- data/spec/rubocop/cops/empty_line_between_defs_spec.rb +83 -0
- data/spec/rubocop/cops/empty_lines_spec.rb +6 -63
- data/spec/rubocop/cops/eval_spec.rb +36 -0
- data/spec/rubocop/cops/favor_join_spec.rb +39 -0
- data/spec/rubocop/cops/hash_literal_spec.rb +8 -1
- data/spec/rubocop/cops/leading_comment_space_spec.rb +60 -0
- data/spec/rubocop/cops/line_continuation_spec.rb +24 -0
- data/spec/rubocop/cops/line_length_spec.rb +1 -0
- data/spec/rubocop/cops/method_and_variable_snake_case_spec.rb +20 -0
- data/spec/rubocop/cops/method_length_spec.rb +2 -5
- data/spec/rubocop/cops/new_lambda_literal_spec.rb +2 -3
- data/spec/rubocop/cops/not_spec.rb +34 -0
- data/spec/rubocop/cops/offence_spec.rb +7 -0
- data/spec/rubocop/cops/semicolon_spec.rb +79 -4
- data/spec/rubocop/cops/single_line_methods_spec.rb +50 -0
- data/spec/rubocop/cops/space_after_control_keyword_spec.rb +28 -0
- data/spec/rubocop/cops/space_around_equals_in_default_parameter_spec.rb +11 -1
- data/spec/rubocop/cops/space_inside_hash_literal_braces_spec.rb +74 -0
- data/spec/rubocop/cops/symbol_array_spec.rb +25 -0
- data/spec/rubocop/cops/trivial_accessors_spec.rb +332 -0
- data/spec/rubocop/cops/variable_interpolation_spec.rb +10 -1
- data/spec/rubocop/cops/word_array_spec.rb +39 -0
- data/spec/spec_helper.rb +16 -9
- data/spec/support/file_helper.rb +21 -0
- data/spec/support/isolated_environment.rb +27 -0
- metadata +66 -6
@@ -0,0 +1,202 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
require 'yaml'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
module Rubocop
|
8
|
+
class Config < DelegateClass(Hash)
|
9
|
+
class ValidationError < StandardError; end
|
10
|
+
|
11
|
+
DOTFILE = '.rubocop.yml'
|
12
|
+
RUBOCOP_HOME = File.realpath(File.join(File.dirname(__FILE__), '..', '..'))
|
13
|
+
DEFAULT_FILE = File.join(RUBOCOP_HOME, 'config', 'default.yml')
|
14
|
+
|
15
|
+
attr_reader :loaded_path
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def load_file(path)
|
19
|
+
hash = YAML.load_file(path)
|
20
|
+
|
21
|
+
base_configs(path, hash['inherit_from']).reverse.each do |base_config|
|
22
|
+
base_config.each do |key, value|
|
23
|
+
if value.is_a?(Hash)
|
24
|
+
hash[key] = hash.has_key?(key) ? merge(value, hash[key]) : value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
hash.delete('inherit_from')
|
30
|
+
config = new(hash, File.realpath(path))
|
31
|
+
config.warn_unless_valid
|
32
|
+
config
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return a recursive merge of two hashes. That is, a normal hash
|
36
|
+
# merge, with the addition that any value that is a hash, and
|
37
|
+
# occurs in both arguments, will also be merged. And so on.
|
38
|
+
def merge(base_hash, derived_hash)
|
39
|
+
result = {}
|
40
|
+
base_hash.each do |key, value|
|
41
|
+
result[key] = if derived_hash.has_key?(key)
|
42
|
+
if value.is_a?(Hash)
|
43
|
+
merge(value, derived_hash[key])
|
44
|
+
else
|
45
|
+
derived_hash[key]
|
46
|
+
end
|
47
|
+
else
|
48
|
+
base_hash[key]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
derived_hash.each do |key, value|
|
52
|
+
result[key] = value unless base_hash.has_key?(key)
|
53
|
+
end
|
54
|
+
result
|
55
|
+
end
|
56
|
+
|
57
|
+
def base_configs(path, inherit_from)
|
58
|
+
base_files = case inherit_from
|
59
|
+
when nil then []
|
60
|
+
when String then [inherit_from]
|
61
|
+
when Array then inherit_from
|
62
|
+
end
|
63
|
+
base_files.map do |f|
|
64
|
+
f = File.join(File.dirname(path), f) unless f.start_with?('/')
|
65
|
+
load_file(f)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the path of .rubocop.yml searching upwards in the
|
70
|
+
# directory structure starting at the given directory where the
|
71
|
+
# inspected file is. If no .rubocop.yml is found there, the
|
72
|
+
# user's home directory is checked. If there's no .rubocop.yml
|
73
|
+
# there either, the path to the default file is returned.
|
74
|
+
def configuration_file_for(target_dir)
|
75
|
+
possible_config_files = dirs_to_search(target_dir).map do |dir|
|
76
|
+
File.join(dir, DOTFILE)
|
77
|
+
end
|
78
|
+
|
79
|
+
found_file = possible_config_files.find do |config_file|
|
80
|
+
File.exist?(config_file)
|
81
|
+
end
|
82
|
+
found_file || DEFAULT_FILE
|
83
|
+
end
|
84
|
+
|
85
|
+
def configuration_from_file(config_file)
|
86
|
+
config = load_file(config_file)
|
87
|
+
merge_with_default(config, config_file)
|
88
|
+
end
|
89
|
+
|
90
|
+
def merge_with_default(config, config_file)
|
91
|
+
default_config = load_file(DEFAULT_FILE)
|
92
|
+
new(merge(default_config, config), config_file)
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def dirs_to_search(target_dir)
|
98
|
+
dirs_to_search = []
|
99
|
+
target_dir_pathname = Pathname.new(File.expand_path(target_dir))
|
100
|
+
target_dir_pathname.ascend do |dir_pathname|
|
101
|
+
dirs_to_search << dir_pathname.to_s
|
102
|
+
end
|
103
|
+
dirs_to_search << Dir.home
|
104
|
+
dirs_to_search
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def initialize(hash = {}, loaded_path = nil)
|
109
|
+
@hash = hash
|
110
|
+
@loaded_path = loaded_path
|
111
|
+
super(@hash)
|
112
|
+
end
|
113
|
+
|
114
|
+
def for_cop(cop)
|
115
|
+
self[cop]
|
116
|
+
end
|
117
|
+
|
118
|
+
def cop_enabled?(cop)
|
119
|
+
self[cop].nil? || self[cop]['Enabled']
|
120
|
+
end
|
121
|
+
|
122
|
+
def warn_unless_valid
|
123
|
+
validate!
|
124
|
+
rescue Config::ValidationError => e
|
125
|
+
puts "Warning: #{e.message}".color(:red)
|
126
|
+
end
|
127
|
+
|
128
|
+
# TODO: This should be a private method
|
129
|
+
def validate!
|
130
|
+
# Don't validate RuboCop's own files. Avoids inifinite recursion.
|
131
|
+
return if @loaded_path.start_with?(RUBOCOP_HOME)
|
132
|
+
|
133
|
+
default_config = Config.load_file(DEFAULT_FILE)
|
134
|
+
|
135
|
+
valid_cop_names, invalid_cop_names = @hash.keys.partition do |key|
|
136
|
+
default_config.has_key?(key)
|
137
|
+
end
|
138
|
+
|
139
|
+
invalid_cop_names.each do |name|
|
140
|
+
fail ValidationError,
|
141
|
+
"unrecognized cop #{name} found in #{loaded_path || self}"
|
142
|
+
end
|
143
|
+
|
144
|
+
valid_cop_names.each do |name|
|
145
|
+
@hash[name].each_key do |param|
|
146
|
+
unless default_config[name].has_key?(param)
|
147
|
+
fail ValidationError,
|
148
|
+
"unrecognized parameter #{name}:#{param} found " +
|
149
|
+
"in #{loaded_path || self}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def file_to_include?(file)
|
156
|
+
relative_file_path = relative_path_to_loaded_dir(file)
|
157
|
+
patterns_to_include.any? do |pattern|
|
158
|
+
match_path?(pattern, relative_file_path)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def file_to_exclude?(file)
|
163
|
+
relative_file_path = relative_path_to_loaded_dir(file)
|
164
|
+
patterns_to_exclude.any? do |pattern|
|
165
|
+
match_path?(pattern, relative_file_path)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def patterns_to_include
|
170
|
+
@hash['AllCops']['Includes']
|
171
|
+
end
|
172
|
+
|
173
|
+
def patterns_to_exclude
|
174
|
+
@hash['AllCops']['Excludes']
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def relative_path_to_loaded_dir(file)
|
180
|
+
return file unless loaded_path
|
181
|
+
file_pathname = Pathname.new(File.expand_path(file))
|
182
|
+
file_pathname.relative_path_from(loaded_dir_pathname).to_s
|
183
|
+
end
|
184
|
+
|
185
|
+
def loaded_dir_pathname
|
186
|
+
return nil unless loaded_path
|
187
|
+
@loaded_dir ||= begin
|
188
|
+
loaded_dir = File.expand_path(File.dirname(loaded_path))
|
189
|
+
Pathname.new(loaded_dir)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def match_path?(pattern, path)
|
194
|
+
case pattern
|
195
|
+
when String
|
196
|
+
File.basename(path) == pattern || File.fnmatch(pattern, path)
|
197
|
+
when Regexp
|
198
|
+
path =~ pattern
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Rubocop
|
4
|
+
module ConfigStore
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def prepare
|
8
|
+
# @options_config stores a config that is specified in the command line.
|
9
|
+
# This takes precedence over configs located in any directories
|
10
|
+
@options_config = nil
|
11
|
+
|
12
|
+
# @path_cache maps directories to configuration paths. We search
|
13
|
+
# for .rubocop.yml only if we haven't already found it for the
|
14
|
+
# given directory.
|
15
|
+
@path_cache = {}
|
16
|
+
|
17
|
+
# @object_cache maps configuration file paths to
|
18
|
+
# configuration objects so we only need to load them once.
|
19
|
+
@object_cache = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_options_config(options_config)
|
23
|
+
loaded_config = Config.load_file(options_config)
|
24
|
+
@options_config = Config.merge_with_default(loaded_config,
|
25
|
+
options_config)
|
26
|
+
end
|
27
|
+
|
28
|
+
def for(file)
|
29
|
+
return @options_config if @options_config
|
30
|
+
|
31
|
+
dir = File.dirname(file)
|
32
|
+
@path_cache[dir] ||= Config.configuration_file_for(dir)
|
33
|
+
path = @path_cache[dir]
|
34
|
+
@object_cache[path] ||= Config.configuration_from_file(path)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/rubocop/cop/alias.rb
CHANGED
@@ -6,11 +6,8 @@ module Rubocop
|
|
6
6
|
ERROR_MESSAGE = 'Use alias_method instead of alias.'
|
7
7
|
|
8
8
|
def inspect(file, source, tokens, sexp)
|
9
|
-
tokens
|
10
|
-
t
|
11
|
-
if t.type == :on_kw && t.text == 'alias'
|
12
|
-
add_offence(:convention, t.pos.lineno, ERROR_MESSAGE)
|
13
|
-
end
|
9
|
+
each_keyword('alias', tokens) do |t|
|
10
|
+
add_offence(:convention, t.pos.lineno, ERROR_MESSAGE)
|
14
11
|
end
|
15
12
|
end
|
16
13
|
end
|
@@ -6,16 +6,55 @@ module Rubocop
|
|
6
6
|
ERROR_MESSAGE = 'Use array literal [] instead of Array.new.'
|
7
7
|
|
8
8
|
def inspect(file, source, tokens, sexp)
|
9
|
+
offences = preliminary_scan(sexp)
|
10
|
+
|
11
|
+
# find Array.new()
|
9
12
|
each(:method_add_arg, sexp) do |s|
|
10
|
-
|
13
|
+
next if s[1][0] != :call
|
14
|
+
|
15
|
+
receiver = s[1][1][1]
|
16
|
+
method_name = s[1][3][1]
|
11
17
|
|
12
|
-
if
|
13
|
-
|
18
|
+
if receiver && receiver[1] == 'Array' &&
|
19
|
+
method_name == 'new' && s[2] == [:arg_paren, nil]
|
20
|
+
offences.delete(Offence.new(:convention,
|
21
|
+
receiver[2].lineno,
|
22
|
+
ERROR_MESSAGE))
|
14
23
|
add_offence(:convention,
|
15
|
-
|
24
|
+
receiver[2].lineno,
|
16
25
|
ERROR_MESSAGE)
|
17
26
|
end
|
27
|
+
|
28
|
+
# check for false positives
|
29
|
+
if receiver && receiver[1] == 'Array' &&
|
30
|
+
method_name == 'new' && s[2] != [:arg_paren, nil]
|
31
|
+
offences.delete(Offence.new(:convention,
|
32
|
+
receiver[2].lineno,
|
33
|
+
ERROR_MESSAGE))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
offences.each { |o| add_offence(*o.explode) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def preliminary_scan(sexp)
|
41
|
+
offences = []
|
42
|
+
|
43
|
+
# find Array.new
|
44
|
+
# there will be some false positives here, which
|
45
|
+
# we'll eliminate later on
|
46
|
+
each(:call, sexp) do |s|
|
47
|
+
receiver = s[1][1]
|
48
|
+
|
49
|
+
if receiver && receiver[1] == 'Array' &&
|
50
|
+
s[3][1] == 'new'
|
51
|
+
offences << Offence.new(:convention,
|
52
|
+
receiver[2].lineno,
|
53
|
+
ERROR_MESSAGE)
|
54
|
+
end
|
18
55
|
end
|
56
|
+
|
57
|
+
offences
|
19
58
|
end
|
20
59
|
end
|
21
60
|
end
|
@@ -6,10 +6,8 @@ module Rubocop
|
|
6
6
|
ERROR_MESSAGE = 'Prefer *each* over *for*.'
|
7
7
|
|
8
8
|
def inspect(file, source, tokens, sexp)
|
9
|
-
|
10
|
-
add_offence(:convention,
|
11
|
-
s[1][1][2].lineno,
|
12
|
-
ERROR_MESSAGE)
|
9
|
+
each_keyword('for', tokens) do |t|
|
10
|
+
add_offence(:convention, t.pos.lineno, ERROR_MESSAGE)
|
13
11
|
end
|
14
12
|
end
|
15
13
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Rubocop
|
4
|
+
module Cop
|
5
|
+
class AvoidGlobalVars < Cop
|
6
|
+
ERROR_MESSAGE = 'Do not introduce global variables.'
|
7
|
+
|
8
|
+
# predefined global variables their English aliases
|
9
|
+
# http://www.zenspider.com/Languages/Ruby/QuickRef.html
|
10
|
+
BUILT_IN_VARS = %w(
|
11
|
+
$: $LOAD_PATH
|
12
|
+
$" $LOADED_FEATURES
|
13
|
+
$0 $PROGRAM_NAME
|
14
|
+
$! $ERROR_INFO
|
15
|
+
$@ $ERROR_POSITION
|
16
|
+
$; $FS $FIELD_SEPARATOR
|
17
|
+
$, $OFS $OUTPUT_FIELD_SEPARATOR
|
18
|
+
$/ $RS $INPUT_RECORD_SEPARATOR
|
19
|
+
$\\ $ORS $OUTPUT_RECORD_SEPARATOR
|
20
|
+
$. $NR $INPUT_LINE_NUMBER
|
21
|
+
$_ $LAST_READ_LINE
|
22
|
+
$> $DEFAULT_OUTPUT
|
23
|
+
$< $DEFAULT_INPUT
|
24
|
+
$$ $PID $PROCESS_ID
|
25
|
+
$? $CHILD_STATUS
|
26
|
+
$~ $LAST_MATCH_INFO
|
27
|
+
$= $IGNORECASE
|
28
|
+
$* $ARGV
|
29
|
+
$& $MATCH
|
30
|
+
$` $PREMATCH
|
31
|
+
$' $POSTMATCH
|
32
|
+
$+ $LAST_PAREN_MATCH
|
33
|
+
$stdin $stdout $stderr
|
34
|
+
$DEBUG $FILENAME $VERBOSE
|
35
|
+
$-0 $-a $-d $-F $-i $-I $-l $-p $-v $-w
|
36
|
+
)
|
37
|
+
|
38
|
+
def inspect(file, source, tokens, sexp)
|
39
|
+
each(:@gvar, sexp) do |s|
|
40
|
+
global_var = s[1]
|
41
|
+
|
42
|
+
unless BUILT_IN_VARS.include?(global_var)
|
43
|
+
add_offence(:convention, s[2].lineno, ERROR_MESSAGE)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Rubocop
|
4
|
+
module Cop
|
5
|
+
class BlockComments < Cop
|
6
|
+
ERROR_MESSAGE = 'Do not use block comments.'
|
7
|
+
|
8
|
+
def inspect(file, source, tokens, sexp)
|
9
|
+
tokens.each do |t|
|
10
|
+
if t.type == :on_embdoc_beg
|
11
|
+
add_offence(:convention, t.pos.lineno, ERROR_MESSAGE)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -5,9 +5,11 @@ module Rubocop
|
|
5
5
|
class BraceAfterPercent < Cop
|
6
6
|
ERROR_MESSAGE = 'Prefer () as delimiters for all % literals.'
|
7
7
|
LITERALS = {
|
8
|
-
on_tstring_beg: '%q',
|
8
|
+
on_tstring_beg: ['%q', '%Q'],
|
9
9
|
on_words_beg: '%W',
|
10
10
|
on_qwords_beg: '%w',
|
11
|
+
on_qsymbols_beg: '%i',
|
12
|
+
on_symbols_beg: '%I',
|
11
13
|
on_regexp_beg: '%r',
|
12
14
|
on_symbeg: '%s',
|
13
15
|
on_backtick: '%x'
|
@@ -16,10 +18,12 @@ module Rubocop
|
|
16
18
|
def inspect(file, source, tokens, sexp)
|
17
19
|
tokens.each_index do |ix|
|
18
20
|
t = tokens[ix]
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
literals = Array(LITERALS[t.type])
|
22
|
+
literals.each do |literal|
|
23
|
+
if literal && t.text.start_with?(literal) && t.text[2] != '('
|
24
|
+
add_offence(:convention, t.pos.lineno,
|
25
|
+
ERROR_MESSAGE)
|
26
|
+
end
|
23
27
|
end
|
24
28
|
end
|
25
29
|
end
|