rubocop-airbnb 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +7 -0
- data/LICENSE.md +9 -0
- data/README.md +68 -0
- data/config/default.yml +39 -0
- data/config/rubocop-airbnb.yml +96 -0
- data/config/rubocop-bundler.yml +8 -0
- data/config/rubocop-gemspec.yml +9 -0
- data/config/rubocop-layout.yml +514 -0
- data/config/rubocop-lint.yml +315 -0
- data/config/rubocop-metrics.yml +47 -0
- data/config/rubocop-naming.yml +68 -0
- data/config/rubocop-performance.yml +143 -0
- data/config/rubocop-rails.yml +193 -0
- data/config/rubocop-rspec.yml +281 -0
- data/config/rubocop-security.yml +13 -0
- data/config/rubocop-style.yml +953 -0
- data/lib/rubocop-airbnb.rb +11 -0
- data/lib/rubocop/airbnb.rb +16 -0
- data/lib/rubocop/airbnb/inflections.rb +14 -0
- data/lib/rubocop/airbnb/inject.rb +20 -0
- data/lib/rubocop/airbnb/rails_autoloading.rb +55 -0
- data/lib/rubocop/airbnb/version.rb +8 -0
- data/lib/rubocop/cop/airbnb/class_name.rb +47 -0
- data/lib/rubocop/cop/airbnb/class_or_module_declared_in_wrong_file.rb +138 -0
- data/lib/rubocop/cop/airbnb/const_assigned_in_wrong_file.rb +74 -0
- data/lib/rubocop/cop/airbnb/continuation_slash.rb +25 -0
- data/lib/rubocop/cop/airbnb/default_scope.rb +20 -0
- data/lib/rubocop/cop/airbnb/factory_attr_references_class.rb +74 -0
- data/lib/rubocop/cop/airbnb/factory_class_use_string.rb +39 -0
- data/lib/rubocop/cop/airbnb/mass_assignment_accessible_modifier.rb +18 -0
- data/lib/rubocop/cop/airbnb/module_method_in_wrong_file.rb +104 -0
- data/lib/rubocop/cop/airbnb/no_timeout.rb +19 -0
- data/lib/rubocop/cop/airbnb/opt_arg_parameters.rb +38 -0
- data/lib/rubocop/cop/airbnb/phrase_bundle_keys.rb +67 -0
- data/lib/rubocop/cop/airbnb/risky_activerecord_invocation.rb +63 -0
- data/lib/rubocop/cop/airbnb/rspec_describe_or_context_under_namespace.rb +114 -0
- data/lib/rubocop/cop/airbnb/rspec_environment_modification.rb +58 -0
- data/lib/rubocop/cop/airbnb/simple_modifier_conditional.rb +23 -0
- data/lib/rubocop/cop/airbnb/spec_constant_assignment.rb +55 -0
- data/lib/rubocop/cop/airbnb/unsafe_yaml_marshal.rb +47 -0
- data/rubocop-airbnb.gemspec +32 -0
- data/spec/rubocop/cop/airbnb/class_name_spec.rb +78 -0
- data/spec/rubocop/cop/airbnb/class_or_module_declared_in_wrong_file_spec.rb +174 -0
- data/spec/rubocop/cop/airbnb/const_assigned_in_wrong_file_spec.rb +178 -0
- data/spec/rubocop/cop/airbnb/continuation_slash_spec.rb +162 -0
- data/spec/rubocop/cop/airbnb/default_scope_spec.rb +38 -0
- data/spec/rubocop/cop/airbnb/factory_attr_references_class_spec.rb +160 -0
- data/spec/rubocop/cop/airbnb/factory_class_use_string_spec.rb +26 -0
- data/spec/rubocop/cop/airbnb/mass_assignment_accessible_modifier_spec.rb +28 -0
- data/spec/rubocop/cop/airbnb/module_method_in_wrong_file_spec.rb +181 -0
- data/spec/rubocop/cop/airbnb/no_timeout_spec.rb +30 -0
- data/spec/rubocop/cop/airbnb/opt_arg_parameter_spec.rb +103 -0
- data/spec/rubocop/cop/airbnb/phrase_bundle_keys_spec.rb +74 -0
- data/spec/rubocop/cop/airbnb/risky_activerecord_invocation_spec.rb +54 -0
- data/spec/rubocop/cop/airbnb/rspec_describe_or_context_under_namespace_spec.rb +284 -0
- data/spec/rubocop/cop/airbnb/rspec_environment_modification_spec.rb +64 -0
- data/spec/rubocop/cop/airbnb/simple_modifier_conditional_spec.rb +122 -0
- data/spec/rubocop/cop/airbnb/spec_constant_assignment_spec.rb +80 -0
- data/spec/rubocop/cop/airbnb/unsafe_yaml_marshal_spec.rb +50 -0
- data/spec/spec_helper.rb +35 -0
- metadata +150 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Cop
|
3
|
+
module Airbnb
|
4
|
+
# Cop to tell developers to use :class => "MyClass" instead of :class => MyClass,
|
5
|
+
# because the latter slows down reloading zeus.
|
6
|
+
class FactoryClassUseString < Cop
|
7
|
+
MSG = 'Instead of :class => MyClass, use :class => "MyClass". ' \
|
8
|
+
"This enables faster spec startup time and faster Zeus reload time.".freeze
|
9
|
+
|
10
|
+
def on_send(node)
|
11
|
+
return unless node.command?(:factory)
|
12
|
+
|
13
|
+
class_pair = class_node(node)
|
14
|
+
|
15
|
+
if class_pair && !string_class_name?(class_pair)
|
16
|
+
add_offense(class_pair)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Return the descendant node that is a hash pair (:key => value) whose key
|
23
|
+
# is :class.
|
24
|
+
def class_node(node)
|
25
|
+
node.descendants.detect do |e|
|
26
|
+
e.is_a?(Parser::AST::Node) &&
|
27
|
+
e.pair_type? &&
|
28
|
+
e.children[0].children[0] == :class
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Given a hash pair :class_name => value, is the value a hardcoded string?
|
33
|
+
def string_class_name?(class_pair)
|
34
|
+
class_pair.children[1].str_type?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Cop
|
3
|
+
module Airbnb
|
4
|
+
# Modifying Mass assignment restrictions eliminates the entire point of disabling
|
5
|
+
# mass assignment. It's a lazy, potentially dangerous approach that should be discouraged.
|
6
|
+
class MassAssignmentAccessibleModifier < Cop
|
7
|
+
MSG = 'Do no override and objects mass assignment restrictions.'.freeze
|
8
|
+
|
9
|
+
def on_send(node)
|
10
|
+
_receiver, method_name, *_args = *node
|
11
|
+
|
12
|
+
return unless method_name == :accessible=
|
13
|
+
add_offense(node, message: MSG)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require_relative '../../airbnb/inflections'
|
2
|
+
require_relative '../../airbnb/rails_autoloading'
|
3
|
+
|
4
|
+
module RuboCop
|
5
|
+
module Cop
|
6
|
+
module Airbnb
|
7
|
+
# This cop checks for methods defined in a module declaration, in a file that doesn't
|
8
|
+
# match the module name. The Rails autoloader can't find such a method, but sometimes
|
9
|
+
# people "get lucky" if the file happened to be loaded before the method was defined.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# # bad
|
13
|
+
#
|
14
|
+
# # foo/bar.rb
|
15
|
+
# module Foo
|
16
|
+
# class Bar
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def baz
|
20
|
+
# 42
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # good
|
25
|
+
#
|
26
|
+
# # foo/bar.rb
|
27
|
+
# module Foo
|
28
|
+
# class Bar
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # foo.rb
|
33
|
+
# module Foo
|
34
|
+
# def baz
|
35
|
+
# 42
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# Note that autoloading works fine if classes are defined in the file that defines
|
40
|
+
# the module. This is common usage for things like error classes, so we'll allow it:
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# # good
|
44
|
+
#
|
45
|
+
# # foo.rb
|
46
|
+
# module Foo
|
47
|
+
# class Bar < StandardError
|
48
|
+
# def baz
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# # good
|
54
|
+
#
|
55
|
+
# # foo.rb
|
56
|
+
# class Foo
|
57
|
+
# class Bar # nested class
|
58
|
+
# def baz
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
class ModuleMethodInWrongFile < Cop
|
63
|
+
include Inflections
|
64
|
+
include RailsAutoloading
|
65
|
+
|
66
|
+
MSG_TEMPLATE =
|
67
|
+
"In order for Rails autoloading to be able to find and load this file when " \
|
68
|
+
"someone calls this method, move the method definition to a file that defines " \
|
69
|
+
"the module. This file just uses the module as a namespace for another class " \
|
70
|
+
"or module. Method %s should be defined in %s.".freeze
|
71
|
+
|
72
|
+
def on_def(node)
|
73
|
+
method_name, args, body = *node
|
74
|
+
on_method_def(node, method_name, args, body)
|
75
|
+
end
|
76
|
+
|
77
|
+
alias on_defs on_def
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def on_method_def(node, method_name, args, body)
|
82
|
+
path = node.source_range.source_buffer.name
|
83
|
+
return unless run_rails_autoloading_cops?(path)
|
84
|
+
return unless node.parent_module_name
|
85
|
+
# "#<Class:>" is the parent module name of a method being defined in an if/unless.
|
86
|
+
return if node.parent_module_name == "#<Class:>"
|
87
|
+
|
88
|
+
expected_dir = underscore(normalize_module_name(node.parent_module_name))
|
89
|
+
allowable_filenames = expected_dir.split("/").map { |file| "#{file}.rb" }
|
90
|
+
basename = File.basename(path)
|
91
|
+
if !allowable_filenames.include?(basename)
|
92
|
+
parent_module_names = split_modules(node.parent_module_name)
|
93
|
+
expected_parent_module_file =
|
94
|
+
"#{parent_module_names.map { |name| underscore(name) }.join("/")}.rb"
|
95
|
+
add_offense(
|
96
|
+
node,
|
97
|
+
message: MSG_TEMPLATE % [method_name, expected_parent_module_file]
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Cop
|
3
|
+
module Airbnb
|
4
|
+
class NoTimeout < Cop
|
5
|
+
MSG =
|
6
|
+
'Do not use Timeout.timeout. In combination with Rails autoloading, ' \
|
7
|
+
'timeout can cause Segmentation Faults in version of Ruby we use. ' \
|
8
|
+
'It can also cause logic errors since it can raise in ' \
|
9
|
+
'any callee scope. Use client library timeouts and monitoring to ' \
|
10
|
+
'ensure proper timing behavior for web requests.'.freeze
|
11
|
+
|
12
|
+
def on_send(node)
|
13
|
+
return unless node.source.start_with?('Timeout.timeout')
|
14
|
+
add_offense(node, message: MSG)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Cop
|
3
|
+
module Airbnb
|
4
|
+
# Cop to enforce use of options hash over default arguments
|
5
|
+
# https://github.com/airbnb/ruby#no-default-args
|
6
|
+
class OptArgParameters < Cop
|
7
|
+
MSG =
|
8
|
+
'Do not use default positional arguments. '\
|
9
|
+
'Use keyword arguments or an options hash instead.'.freeze
|
10
|
+
|
11
|
+
def on_args(node)
|
12
|
+
*but_last, last_arg = *node
|
13
|
+
|
14
|
+
if last_arg && last_arg.blockarg_type?
|
15
|
+
last_arg = but_last.pop
|
16
|
+
end
|
17
|
+
|
18
|
+
but_last.each do |arg|
|
19
|
+
next unless arg.optarg_type?
|
20
|
+
add_offense(arg, message: MSG)
|
21
|
+
end
|
22
|
+
return if last_arg.nil?
|
23
|
+
|
24
|
+
return unless last_arg.optarg_type?
|
25
|
+
|
26
|
+
_arg_name, default_value = *last_arg
|
27
|
+
if default_value.hash_type?
|
28
|
+
# asserting default value is empty hash
|
29
|
+
*key_value_pairs = *default_value
|
30
|
+
return if key_value_pairs.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
add_offense(last_arg, message: MSG)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Cop
|
3
|
+
module Airbnb
|
4
|
+
# Prefer matching Phrase Bundle and t call keys inside of
|
5
|
+
# PhraseBundleClasses.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# # bad
|
9
|
+
# def phrases
|
10
|
+
# {
|
11
|
+
# "shortened_key" => t(
|
12
|
+
# "my_real_translation_key",
|
13
|
+
# default: 'Does not matter',
|
14
|
+
# ),
|
15
|
+
# }
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # good
|
19
|
+
# def phrases
|
20
|
+
# {
|
21
|
+
# "my_real_translation_key" => t(
|
22
|
+
# "my_real_translation_key",
|
23
|
+
# default: 'Does not matter',
|
24
|
+
# ),
|
25
|
+
# }
|
26
|
+
# end
|
27
|
+
class PhraseBundleKeys < Cop
|
28
|
+
MESSAGE =
|
29
|
+
'Phrase bundle keys should match their translation keys.'.freeze
|
30
|
+
|
31
|
+
def on_send(node)
|
32
|
+
parent = node.parent
|
33
|
+
if t_call?(node) && in_phrase_bundle_class?(node) && parent.pair_type?
|
34
|
+
hash_key = parent.children[0]
|
35
|
+
unless hash_key.children[0] == node.children[2].children[0]
|
36
|
+
add_offense(hash_key, message: MESSAGE)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def in_phrase_bundle_class?(node)
|
44
|
+
if node.class_type? && !const_phrase_bundle_children(node).empty?
|
45
|
+
true
|
46
|
+
elsif node.parent
|
47
|
+
in_phrase_bundle_class?(node.parent)
|
48
|
+
else
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def const_phrase_bundle_children(node)
|
54
|
+
node.children.select do |e|
|
55
|
+
e.is_a?(Parser::AST::Node) &&
|
56
|
+
e.const_type? &&
|
57
|
+
e.children[1] == :PhraseBundle
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def t_call?(node)
|
62
|
+
node.children[1] == :t
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Cop
|
3
|
+
module Airbnb
|
4
|
+
# Disallow ActiveRecord calls that pass interpolated or added strings as an argument.
|
5
|
+
class RiskyActiverecordInvocation < Cop
|
6
|
+
VULNERABLE_AR_METHODS = [
|
7
|
+
:delete_all,
|
8
|
+
:destroy_all,
|
9
|
+
:exists?,
|
10
|
+
:execute,
|
11
|
+
:find_by_sql,
|
12
|
+
:group,
|
13
|
+
:having,
|
14
|
+
:insert,
|
15
|
+
:order,
|
16
|
+
:pluck,
|
17
|
+
:reorder,
|
18
|
+
:select,
|
19
|
+
:select_rows,
|
20
|
+
:select_values,
|
21
|
+
:select_all,
|
22
|
+
:update_all,
|
23
|
+
:where,
|
24
|
+
].freeze
|
25
|
+
MSG = 'Passing a string computed by interpolation or addition to an ActiveRecord ' \
|
26
|
+
'method is likely to lead to SQL injection. Use hash or parameterized syntax. For ' \
|
27
|
+
'more information, see ' \
|
28
|
+
'http://guides.rubyonrails.org/security.html#sql-injection-countermeasures and ' \
|
29
|
+
'https://rails-sqli.org/rails3. If you have confirmed with Security that this is a ' \
|
30
|
+
'safe usage of this style, disable this alert with ' \
|
31
|
+
'`# rubocop:disable Airbnb/RiskyActiverecordInvocation`.'.freeze
|
32
|
+
def on_send(node)
|
33
|
+
receiver, method_name, *_args = *node
|
34
|
+
|
35
|
+
return if receiver.nil?
|
36
|
+
return unless vulnerable_ar_method?(method_name)
|
37
|
+
if !includes_interpolation?(_args) && !includes_sum?(_args)
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
add_offense(node)
|
42
|
+
end
|
43
|
+
|
44
|
+
def vulnerable_ar_method?(method)
|
45
|
+
VULNERABLE_AR_METHODS.include?(method)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return true if the first arg is a :dstr that has non-:str components
|
49
|
+
def includes_interpolation?(args)
|
50
|
+
!args.first.nil? &&
|
51
|
+
args.first.type == :dstr &&
|
52
|
+
args.first.each_child_node.any? { |child| child.type != :str }
|
53
|
+
end
|
54
|
+
|
55
|
+
def includes_sum?(args)
|
56
|
+
!args.first.nil? &&
|
57
|
+
args.first.type == :send &&
|
58
|
+
args.first.method_name == :+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Cop
|
3
|
+
module Airbnb
|
4
|
+
# This cop checks for Rspec describe or context method calls under a namespace.
|
5
|
+
# It can potentially cause autoloading to occur in a different order than it
|
6
|
+
# would have in development or production. This could cause flaky tests.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # bad
|
10
|
+
#
|
11
|
+
# # spec/foo/bar_spec.rb
|
12
|
+
# module Foo
|
13
|
+
# describe Bar do
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
#
|
19
|
+
# # spec/foo/bar_spec.rb do
|
20
|
+
#
|
21
|
+
# describe Foo::Bar
|
22
|
+
# end
|
23
|
+
class RspecDescribeOrContextUnderNamespace < Cop
|
24
|
+
DESCRIBE_OR_CONTEXT_UNDER_NAMESPACE_MSG =
|
25
|
+
'Declaring a `module` in a spec can break autoloading because subsequent references ' \
|
26
|
+
'to it will not cause it to be loaded from the app. This could cause flaky tests.'.freeze
|
27
|
+
|
28
|
+
FIX_DESCRIBE_OR_CONTEXT_HELP_MSG =
|
29
|
+
'Change `%{describe} %{klass} do` to `%{describe} %{module_name}::%{klass} do`.'.freeze
|
30
|
+
|
31
|
+
FIX_CODE_HELP_MSG =
|
32
|
+
'Remove `module %{module_name}` and fix `%{module_name}::CONST` and ' \
|
33
|
+
'`%{module_name}.method` calls accordingly.'.freeze
|
34
|
+
|
35
|
+
def_node_matcher :describe_or_context?,
|
36
|
+
'(send {(const nil? :RSpec) nil?} {:describe :context} ...)'.freeze
|
37
|
+
|
38
|
+
def on_module(node)
|
39
|
+
path = node.source_range.source_buffer.name
|
40
|
+
return unless is_spec_file?(path)
|
41
|
+
|
42
|
+
matched_node = search_children_for_describe_or_context(node.children)
|
43
|
+
return unless matched_node
|
44
|
+
|
45
|
+
method_name = matched_node.method_name
|
46
|
+
module_name = get_module_name(node)
|
47
|
+
message = [DESCRIBE_OR_CONTEXT_UNDER_NAMESPACE_MSG]
|
48
|
+
|
49
|
+
described_class = get_described_class(matched_node)
|
50
|
+
method_parent = get_method_parent(matched_node)
|
51
|
+
parent_dot_method = method_parent ? "#{method_parent}.#{method_name}" : method_name
|
52
|
+
if described_class
|
53
|
+
message << FIX_DESCRIBE_OR_CONTEXT_HELP_MSG % {
|
54
|
+
describe: parent_dot_method,
|
55
|
+
klass: described_class,
|
56
|
+
module_name: module_name,
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
message << FIX_CODE_HELP_MSG % { module_name: module_name }
|
61
|
+
add_offense(node, message: message.join(' '))
|
62
|
+
end
|
63
|
+
|
64
|
+
def search_children_for_describe_or_context(nodes)
|
65
|
+
blocks = []
|
66
|
+
# match nodes for send describe or context
|
67
|
+
nodes.detect do |node|
|
68
|
+
next unless node
|
69
|
+
|
70
|
+
if is_block?(node)
|
71
|
+
blocks << node
|
72
|
+
next
|
73
|
+
end
|
74
|
+
return node if describe_or_context?(node)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Process child nodes of block
|
78
|
+
blocks.each do |node|
|
79
|
+
matched_node = search_children_for_describe_or_context(node.children)
|
80
|
+
return matched_node if matched_node
|
81
|
+
end
|
82
|
+
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def is_spec_file?(path)
|
87
|
+
path.end_with?('_spec.rb')
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_module_name(node)
|
91
|
+
const_node = node.children[0]
|
92
|
+
return unless const_node
|
93
|
+
const_node.const_name
|
94
|
+
end
|
95
|
+
|
96
|
+
def get_described_class(node)
|
97
|
+
const_node = node.children[2]
|
98
|
+
return unless const_node
|
99
|
+
const_node.const_name
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_method_parent(node)
|
103
|
+
const_node = node.children[0]
|
104
|
+
return unless const_node
|
105
|
+
const_node.const_name
|
106
|
+
end
|
107
|
+
|
108
|
+
def is_block?(node)
|
109
|
+
node && [:block, :begin].include?(node.type)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|