rubocop-gusto 10.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/CHANGELOG.md +4 -0
- data/LICENSE +21 -0
- data/README.md +53 -0
- data/config/default.yml +781 -0
- data/config/rails.yml +122 -0
- data/exe/gusto-rubocop +12 -0
- data/exe/rubocop-gusto +9 -0
- data/lib/rubocop/cop/gusto/bootsnap_load_file.rb +57 -0
- data/lib/rubocop/cop/gusto/datadog_constant.rb +16 -0
- data/lib/rubocop/cop/gusto/execute_migration.rb +16 -0
- data/lib/rubocop/cop/gusto/factory_classes_or_modules.rb +19 -0
- data/lib/rubocop/cop/gusto/min_by_max_by.rb +45 -0
- data/lib/rubocop/cop/gusto/no_metaprogramming.rb +131 -0
- data/lib/rubocop/cop/gusto/no_rescue_error_message_checking.rb +66 -0
- data/lib/rubocop/cop/gusto/no_send.rb +32 -0
- data/lib/rubocop/cop/gusto/object_in.rb +36 -0
- data/lib/rubocop/cop/gusto/paperclip_or_attachable.rb +17 -0
- data/lib/rubocop/cop/gusto/perform_class_method.rb +73 -0
- data/lib/rubocop/cop/gusto/polymorphic_type_validation.rb +89 -0
- data/lib/rubocop/cop/gusto/prefer_process_last_status.rb +35 -0
- data/lib/rubocop/cop/gusto/rabl_extends.rb +43 -0
- data/lib/rubocop/cop/gusto/rails_env.rb +72 -0
- data/lib/rubocop/cop/gusto/rake_constants.rb +68 -0
- data/lib/rubocop/cop/gusto/regexp_bypass.rb +90 -0
- data/lib/rubocop/cop/gusto/sidekiq_params.rb +21 -0
- data/lib/rubocop/cop/gusto/toplevel_constants.rb +55 -0
- data/lib/rubocop/cop/gusto/use_paint_not_colorize.rb +240 -0
- data/lib/rubocop/cop/gusto/vcr_recordings.rb +49 -0
- data/lib/rubocop/cop/internal_affairs/assignment_first.rb +56 -0
- data/lib/rubocop/cop/internal_affairs/require_restrict_on_send.rb +62 -0
- data/lib/rubocop/gusto/cli.rb +22 -0
- data/lib/rubocop/gusto/config_yml.rb +135 -0
- data/lib/rubocop/gusto/init.rb +59 -0
- data/lib/rubocop/gusto/plugin.rb +29 -0
- data/lib/rubocop/gusto/templates/rubocop.yml +25 -0
- data/lib/rubocop/gusto/version.rb +7 -0
- data/lib/rubocop/gusto.rb +9 -0
- data/lib/rubocop-gusto.rb +13 -0
- metadata +178 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This cop enforces that polymorphic relations have a corresponding validation
|
4
|
+
# for their type field with an inclusion validation. This is required in order for Tapioca
|
5
|
+
# to generate correct Sorbet types
|
6
|
+
module RuboCop
|
7
|
+
module Cop
|
8
|
+
module Gusto
|
9
|
+
class PolymorphicTypeValidation < Base
|
10
|
+
RESTRICT_ON_SEND = %i(belongs_to validates polymorphic_methods_for).freeze
|
11
|
+
|
12
|
+
MSG = <<~MESSAGE
|
13
|
+
Polymorphic relations must validate their corresponding type field with "validates .. inclusion: { in: .. }", or using polymorphic_methods_for
|
14
|
+
|
15
|
+
Example:
|
16
|
+
# bad
|
17
|
+
belongs_to :subscription_detail, polymorphic: true
|
18
|
+
|
19
|
+
# good
|
20
|
+
VALID_TYPES = T.let([LidiSubscriptionDetail.polymorphic_name, LegacyActiveBenefits::VoluntaryLifeSubscriptionDetail.polymorphic_name].freeze, T::Array[String])
|
21
|
+
belongs_to :subscription_detail, polymorphic: true
|
22
|
+
validates :subscription_detail_type, presence: true, inclusion: { in: VALID_TYPES }
|
23
|
+
|
24
|
+
# also good (in ZP/HI, at least)
|
25
|
+
include PolymorphicCallable
|
26
|
+
VALID_TYPES = T.let([LidiSubscriptionDetail.polymorphic_name, LegacyActiveBenefits::VoluntaryLifeSubscriptionDetail.polymorphic_name].freeze, T::Array[String])
|
27
|
+
belongs_to :subscription_detail, polymorphic: true
|
28
|
+
polymorphic_methods_for :subscription_detail, VALID_TYPES
|
29
|
+
MESSAGE
|
30
|
+
|
31
|
+
ALLOW_BLANK_MSG = 'Polymorphic type validations cannot use allow_blank: true'
|
32
|
+
|
33
|
+
# @!method polymorphic_relation?(node)
|
34
|
+
def_node_matcher :polymorphic_relation?, <<~PATTERN
|
35
|
+
(send nil? :belongs_to _ (hash <(pair (sym :polymorphic) (true)) ...>))
|
36
|
+
PATTERN
|
37
|
+
|
38
|
+
# @!method type_validation?(node)
|
39
|
+
def_node_matcher :type_validation?, <<~PATTERN
|
40
|
+
(send nil? :validates (sym _) (hash <#inclusion_in? ...>))
|
41
|
+
PATTERN
|
42
|
+
|
43
|
+
# @!method inclusion_in?(node)
|
44
|
+
def_node_matcher :inclusion_in?, <<~PATTERN
|
45
|
+
(pair (sym :inclusion) (hash <(pair (sym :in) _) ...>))
|
46
|
+
PATTERN
|
47
|
+
|
48
|
+
# @!method polymorphic_methods_for?(node)
|
49
|
+
def_node_matcher :polymorphic_methods_for?, <<~PATTERN
|
50
|
+
(send nil? :polymorphic_methods_for (sym _) _)
|
51
|
+
PATTERN
|
52
|
+
|
53
|
+
# @!method allow_blank?(node)
|
54
|
+
def_node_matcher :allow_blank?, <<~PATTERN
|
55
|
+
(pair (sym :allow_blank) (true))
|
56
|
+
PATTERN
|
57
|
+
|
58
|
+
def on_send(node)
|
59
|
+
return unless polymorphic_relation?(node)
|
60
|
+
|
61
|
+
relation_name = node.first_argument.value
|
62
|
+
type_field = :"#{relation_name}_type"
|
63
|
+
|
64
|
+
# Look for either a validation of the type field or polymorphic_methods_for
|
65
|
+
has_validation = false
|
66
|
+
has_allow_blank = false
|
67
|
+
|
68
|
+
node.parent.each_node(:send) do |validation_node|
|
69
|
+
if type_validation?(validation_node) && validation_node.first_argument.value == type_field
|
70
|
+
has_validation = true
|
71
|
+
# Check for allow_blank in the validation options
|
72
|
+
validation_node.arguments[1].each_node(:pair) do |pair_node|
|
73
|
+
has_allow_blank = true if allow_blank?(pair_node)
|
74
|
+
end
|
75
|
+
elsif polymorphic_methods_for?(validation_node) && validation_node.first_argument.value == relation_name
|
76
|
+
has_validation = true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
if has_allow_blank
|
81
|
+
add_offense(node, message: ALLOW_BLANK_MSG)
|
82
|
+
elsif !has_validation
|
83
|
+
add_offense(node)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Gusto
|
6
|
+
# Don't use the `$?` or `$CHILD_STATUS` global variables. Instead, use `Process.last_status`
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # bad
|
10
|
+
# $?.exitstatus
|
11
|
+
# $CHILD_STATUS.success?
|
12
|
+
#
|
13
|
+
# # good
|
14
|
+
# Process.last_status.exit_status
|
15
|
+
# Process.last_status.success?
|
16
|
+
#
|
17
|
+
class PreferProcessLastStatus < Base
|
18
|
+
extend AutoCorrector
|
19
|
+
|
20
|
+
MSG = 'Prefer using `Process.last_status` instead of the global variables: `$?` and `$CHILD_STATUS`.'
|
21
|
+
OFFENDERS = Set[:$?, :$CHILD_STATUS].freeze
|
22
|
+
|
23
|
+
def on_gvar(node)
|
24
|
+
return unless OFFENDERS.include?(node.node_parts.first)
|
25
|
+
|
26
|
+
add_offense(node) { |corrector| autocorrect(corrector, node) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def autocorrect(corrector, node)
|
30
|
+
corrector.replace(node, 'Process.last_status')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Gusto
|
6
|
+
# Disallows the use of `extends` in Rabl templates due to poor caching performance.
|
7
|
+
# Inline the templating to generate your JSON instead.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# extends 'path/to/template'
|
12
|
+
#
|
13
|
+
# # bad - but not covered by this rule
|
14
|
+
# partial 'path/to/template'
|
15
|
+
#
|
16
|
+
# # good - inline your templating
|
17
|
+
# node 'some_node'
|
18
|
+
# attributes :foo, :bar
|
19
|
+
# child(:baz) { attributes :qux }
|
20
|
+
#
|
21
|
+
class RablExtends < Base
|
22
|
+
MSG = 'Avoid using Rabl extends as it has poor caching performance. Inline your JSON instead.'
|
23
|
+
RABL_EXTENSION = '.rabl'
|
24
|
+
RESTRICT_ON_SEND = %i(extends).freeze
|
25
|
+
|
26
|
+
# @!method rabl_extends?(node)
|
27
|
+
def_node_matcher :rabl_extends?, <<~PATTERN
|
28
|
+
(send nil? :extends (str _) ...)
|
29
|
+
PATTERN
|
30
|
+
|
31
|
+
def on_send(node)
|
32
|
+
return unless rabl_extends?(node)
|
33
|
+
|
34
|
+
add_offense(node)
|
35
|
+
end
|
36
|
+
|
37
|
+
def relevant_file?(file)
|
38
|
+
file.end_with?(RABL_EXTENSION)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Gusto
|
6
|
+
# NOTE: Being pushed upstream here: https://github.com/rubocop/rubocop-rails/pull/1375
|
7
|
+
# Checks for usage of `Rails.env` which can be replaced with Feature Flags
|
8
|
+
#
|
9
|
+
# Although `local?` is a form of an environment-specific check, it is allowed because
|
10
|
+
# it cannot be used to control overall environment rollout, but it can be helpful to
|
11
|
+
# distinguish or protect code that is explicitly written to only ever execute in a
|
12
|
+
# dev or test environment. `local?` is also a form of a feature flag.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
#
|
16
|
+
# # bad
|
17
|
+
# Rails.env.production? || Rails.env.demo?
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
# if FeatureFlag.enabled?(:new_feature)
|
21
|
+
# # new feature code
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # good
|
25
|
+
# raise unless Rails.env.local?
|
26
|
+
#
|
27
|
+
# # good
|
28
|
+
# abort ("The Rails environment is running in production mode!") unless Rails.env.local?
|
29
|
+
#
|
30
|
+
class RailsEnv < Base
|
31
|
+
# This allow list is derived from:
|
32
|
+
# (Rails.env.methods - Object.instance_methods).select { |m| m.to_s.end_with?('?') }
|
33
|
+
# and then removing the environment specific methods like development?, test?, production?
|
34
|
+
ALLOWED_LIST = Set.new(
|
35
|
+
%i(
|
36
|
+
unicode_normalized?
|
37
|
+
exclude?
|
38
|
+
empty?
|
39
|
+
acts_like_string?
|
40
|
+
include?
|
41
|
+
is_utf8?
|
42
|
+
casecmp?
|
43
|
+
match?
|
44
|
+
starts_with?
|
45
|
+
ends_with?
|
46
|
+
start_with?
|
47
|
+
end_with?
|
48
|
+
valid_encoding?
|
49
|
+
ascii_only?
|
50
|
+
between?
|
51
|
+
local?
|
52
|
+
)
|
53
|
+
).freeze
|
54
|
+
MSG = 'Use Feature Flags or config instead of `Rails.env`.'
|
55
|
+
PROHIBITED_CLASS = 'Rails'
|
56
|
+
RESTRICT_ON_SEND = %i(env).freeze
|
57
|
+
|
58
|
+
def on_send(node)
|
59
|
+
return unless node.receiver&.const_name == PROHIBITED_CLASS
|
60
|
+
|
61
|
+
return unless (parent = node.parent)
|
62
|
+
return unless parent.send_type?
|
63
|
+
return unless parent.predicate_method?
|
64
|
+
|
65
|
+
return if ALLOWED_LIST.include?(parent.method_name)
|
66
|
+
|
67
|
+
add_offense(parent)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Gusto
|
6
|
+
# Detects constants in a rake file because they are defined at the top level.
|
7
|
+
# It is confusing because the scope looks like it would be in the task or namespace,
|
8
|
+
# but actually it is defined at the top level.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# task :foo do
|
13
|
+
# class C
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # bad
|
18
|
+
# namespace :foo do
|
19
|
+
# module M
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # good - It is also defined at the top level,
|
24
|
+
# # but it looks like intended behavior.
|
25
|
+
# class C
|
26
|
+
# end
|
27
|
+
# task :foo do
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
class RakeConstants < Base
|
31
|
+
MSG = 'Do not define a constant in rake file, because they are sometimes `load`ed, instead of `require`d which can lead to warnings about redefining constants'
|
32
|
+
|
33
|
+
# @!method task_or_namespace?(node)
|
34
|
+
def_node_matcher :task_or_namespace?, <<-PATTERN
|
35
|
+
(block
|
36
|
+
(send _ {:task :namespace} ...)
|
37
|
+
args
|
38
|
+
_
|
39
|
+
)
|
40
|
+
PATTERN
|
41
|
+
|
42
|
+
def on_casgn(node)
|
43
|
+
return unless in_task_or_namespace?(node)
|
44
|
+
|
45
|
+
add_offense(node)
|
46
|
+
end
|
47
|
+
|
48
|
+
def on_class(node)
|
49
|
+
return unless in_task_or_namespace?(node)
|
50
|
+
|
51
|
+
add_offense(node)
|
52
|
+
end
|
53
|
+
|
54
|
+
def on_module(node)
|
55
|
+
return unless in_task_or_namespace?(node)
|
56
|
+
|
57
|
+
add_offense(node)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def in_task_or_namespace?(node)
|
63
|
+
node.each_ancestor(:block).any? { |ancestor| task_or_namespace?(ancestor) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Gusto
|
6
|
+
# Ensures that regular expressions use `\A` and `\z` anchors instead
|
7
|
+
# of `^` and `$` when matching the start or end of a string. This is
|
8
|
+
# critical for security validations as `^` and `$` will match the start/end
|
9
|
+
# of any line in a string, not just the start/end of the entire string.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# # bad - validating only a single line of input which could be split across multiple lines
|
13
|
+
# /^foo/
|
14
|
+
# /foo$/
|
15
|
+
# /^foo$/
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# /\Afoo/
|
19
|
+
# /foo\z/
|
20
|
+
# /\Afoo\z/
|
21
|
+
#
|
22
|
+
# # good - multiline mode is allowed
|
23
|
+
# /^foo/m
|
24
|
+
# /foo$/m
|
25
|
+
#
|
26
|
+
# # okay - anchors in the middle of the pattern are not flagged
|
27
|
+
# /foo^bar/
|
28
|
+
# /foo$bar/
|
29
|
+
#
|
30
|
+
# @safety
|
31
|
+
# We choose to consider this cop safe even though the code is not equivalent.
|
32
|
+
# Replacing `^` and `$` with `\A` and `\z` will make the regex more strict,
|
33
|
+
# which is the intended behavior for securely validating input.
|
34
|
+
#
|
35
|
+
# @see https://ruby-doc.org/core/Regexp.html
|
36
|
+
# @see https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
|
37
|
+
#
|
38
|
+
class RegexpBypass < Base
|
39
|
+
extend AutoCorrector
|
40
|
+
|
41
|
+
MSG = 'Regular expressions matching a single line should use \A instead of ^ and \z instead of $'
|
42
|
+
PROHIBITED_ANCHOR = '^'
|
43
|
+
PROHIBITED_END_ANCHOR = '$'
|
44
|
+
|
45
|
+
def on_regexp(node)
|
46
|
+
return if node.children.find(&:regopt_type?)&.source&.include?('m')
|
47
|
+
|
48
|
+
first_child = node.children.first
|
49
|
+
return unless first_child && !first_child.regopt_type?
|
50
|
+
|
51
|
+
captureless_source = first_child.source.delete('()') # Remove parentheses to check actual content
|
52
|
+
return unless captureless_source.start_with?(PROHIBITED_ANCHOR) || captureless_source.end_with?(PROHIBITED_END_ANCHOR)
|
53
|
+
|
54
|
+
add_offense(first_child) do |corrector|
|
55
|
+
source_buffer = first_child.source_range.source_buffer
|
56
|
+
actual_source = first_child.source
|
57
|
+
|
58
|
+
if captureless_source.start_with?(PROHIBITED_ANCHOR)
|
59
|
+
# Find the position of ^ within the source, accounting for parentheses
|
60
|
+
caret_pos = actual_source.index(PROHIBITED_ANCHOR)
|
61
|
+
start_pos = first_child.source_range.begin_pos + caret_pos
|
62
|
+
corrector.replace(
|
63
|
+
Parser::Source::Range.new(
|
64
|
+
source_buffer,
|
65
|
+
start_pos,
|
66
|
+
start_pos + 1
|
67
|
+
),
|
68
|
+
'\A'
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
if captureless_source.end_with?(PROHIBITED_END_ANCHOR)
|
73
|
+
# Find the position of $ within the source, accounting for parentheses
|
74
|
+
dollar_pos = actual_source.rindex(PROHIBITED_END_ANCHOR)
|
75
|
+
end_pos = first_child.source_range.begin_pos + dollar_pos + 1
|
76
|
+
corrector.replace(
|
77
|
+
Parser::Source::Range.new(
|
78
|
+
source_buffer,
|
79
|
+
end_pos - 1,
|
80
|
+
end_pos
|
81
|
+
),
|
82
|
+
'\z'
|
83
|
+
)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Gusto
|
6
|
+
class SidekiqParams < Base
|
7
|
+
MSG = 'Sidekiq perform methods cannot take keyword arguments'
|
8
|
+
PROHIBITED_ARG_TYPES = Set.new(%i(kwoptarg kwarg)).freeze
|
9
|
+
|
10
|
+
def on_def(node)
|
11
|
+
return unless node.method?(:perform)
|
12
|
+
return if node.arguments.empty?
|
13
|
+
|
14
|
+
node.arguments.each_child_node do |arg|
|
15
|
+
add_offense(node) if PROHIBITED_ARG_TYPES.include?(arg.type)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Gusto
|
6
|
+
# Checks that no top-level constants (excluding classes and modules)
|
7
|
+
# are defined. This rule exists to prevent accidental pollution of the
|
8
|
+
# global namespace as well as cases where application code has
|
9
|
+
# accidentally depended on test code.
|
10
|
+
#
|
11
|
+
# By default, this check is limited to files in `app/`, `lib/`, and
|
12
|
+
# `spec/` directories, except in the root of `lib/` and in support files
|
13
|
+
# in `spec/support/`.
|
14
|
+
#
|
15
|
+
# @example when in a checked directory
|
16
|
+
# # bad
|
17
|
+
# FOO = 'bar' # lib/foo/bar.rb
|
18
|
+
#
|
19
|
+
# # bad
|
20
|
+
# FOO = 'bar' # app/models/foo.rb
|
21
|
+
#
|
22
|
+
# # bad
|
23
|
+
# FOO = 'bar' # spec/foo.rb
|
24
|
+
#
|
25
|
+
# # good
|
26
|
+
# FOO = 'bar' # spec/spec_helper.rb
|
27
|
+
#
|
28
|
+
# # good
|
29
|
+
# class MyClass # lib/foo/bar.rb
|
30
|
+
# FOO = 'bar'
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# @example when in a `spec/support/` file
|
34
|
+
# # good
|
35
|
+
# FOO = 'bar' # spec/support/foo/bar.rb
|
36
|
+
#
|
37
|
+
# @example when in a `config/` file
|
38
|
+
# # good
|
39
|
+
# FOO = 'bar' # config/initializers/foo.rb
|
40
|
+
#
|
41
|
+
class ToplevelConstants < Base
|
42
|
+
MSG = 'Top-level constants should be defined in an initializer. See https://github.com/Gusto/rubocop-gusto/blob/main/lib/rubocop/cop/gusto/toplevel_constants.rb'
|
43
|
+
|
44
|
+
def on_casgn(node)
|
45
|
+
# Allow nested constants
|
46
|
+
return unless node.parent.nil? || node.ancestors.all?(&:begin_type?)
|
47
|
+
# Allow one-liners like `MyClass::MY_CONSTANT = 10`
|
48
|
+
return unless node.children.first.nil?
|
49
|
+
|
50
|
+
add_offense(node)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|