rubocop-callbacksystems 0.1.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 +21 -0
- data/README.md +78 -0
- data/config/default.yml +590 -0
- data/docs/cops.md +111 -0
- data/lib/rubocop/callbacksystems/autocorrection/batch.rb +135 -0
- data/lib/rubocop/callbacksystems/autocorrection/comment_rescue.rb +112 -0
- data/lib/rubocop/callbacksystems/autocorrection/rehearsal.rb +65 -0
- data/lib/rubocop/callbacksystems/autocorrection/rewrite_permit.rb +14 -0
- data/lib/rubocop/callbacksystems/class_structure/builder_assignment.rb +48 -0
- data/lib/rubocop/callbacksystems/class_structure/class_body.rb +50 -0
- data/lib/rubocop/callbacksystems/class_structure/divergence.rb +31 -0
- data/lib/rubocop/callbacksystems/class_structure/node_visibility.rb +82 -0
- data/lib/rubocop/callbacksystems/class_structure/reference_order.rb +36 -0
- data/lib/rubocop/callbacksystems/class_structure/statement_rank.rb +84 -0
- data/lib/rubocop/callbacksystems/class_structure/statement_run.rb +42 -0
- data/lib/rubocop/callbacksystems/class_structure/visibility.rb +84 -0
- data/lib/rubocop/callbacksystems/cops_document.rb +78 -0
- data/lib/rubocop/callbacksystems/execution/deferred_local_variables.rb +138 -0
- data/lib/rubocop/callbacksystems/execution/discarded_expression.rb +45 -0
- data/lib/rubocop/callbacksystems/execution/eager_evaluation_path.rb +73 -0
- data/lib/rubocop/callbacksystems/execution/immediate.rb +71 -0
- data/lib/rubocop/callbacksystems/execution/local_variable_occurrences.rb +112 -0
- data/lib/rubocop/callbacksystems/hashes/key_transform_detection.rb +108 -0
- data/lib/rubocop/callbacksystems/hashes/keyword_options.rb +37 -0
- data/lib/rubocop/callbacksystems/helpers/bodies.rb +150 -0
- data/lib/rubocop/callbacksystems/helpers/builders.rb +103 -0
- data/lib/rubocop/callbacksystems/helpers/comments.rb +56 -0
- data/lib/rubocop/callbacksystems/helpers/corrections.rb +15 -0
- data/lib/rubocop/callbacksystems/helpers/methods.rb +46 -0
- data/lib/rubocop/callbacksystems/helpers/names.rb +65 -0
- data/lib/rubocop/callbacksystems/helpers/node_types.rb +154 -0
- data/lib/rubocop/callbacksystems/helpers/rails.rb +52 -0
- data/lib/rubocop/callbacksystems/helpers/ranges.rb +107 -0
- data/lib/rubocop/callbacksystems/helpers/recursion.rb +29 -0
- data/lib/rubocop/callbacksystems/helpers/variables.rb +44 -0
- data/lib/rubocop/callbacksystems/helpers/visibility.rb +121 -0
- data/lib/rubocop/callbacksystems/helpers.rb +43 -0
- data/lib/rubocop/callbacksystems/methods/calls.rb +38 -0
- data/lib/rubocop/callbacksystems/methods/definitions.rb +218 -0
- data/lib/rubocop/callbacksystems/methods/delegate_macro.rb +144 -0
- data/lib/rubocop/callbacksystems/methods/delegation_target.rb +55 -0
- data/lib/rubocop/callbacksystems/methods/domain.rb +77 -0
- data/lib/rubocop/callbacksystems/methods/implicit_invocations.rb +17 -0
- data/lib/rubocop/callbacksystems/methods/macro_references.rb +238 -0
- data/lib/rubocop/callbacksystems/methods/reference_evidence.rb +74 -0
- data/lib/rubocop/callbacksystems/methods/siblings.rb +24 -0
- data/lib/rubocop/callbacksystems/methods/statement_definitions.rb +68 -0
- data/lib/rubocop/callbacksystems/offense.rb +13 -0
- data/lib/rubocop/callbacksystems/plugin.rb +27 -0
- data/lib/rubocop/callbacksystems/project_index/cache.rb +16 -0
- data/lib/rubocop/callbacksystems/project_index/confinement_cache.rb +40 -0
- data/lib/rubocop/callbacksystems/project_index/constant_references.rb +74 -0
- data/lib/rubocop/callbacksystems/project_index/corrections.rb +35 -0
- data/lib/rubocop/callbacksystems/project_index/coverage.rb +56 -0
- data/lib/rubocop/callbacksystems/project_index/diagnostics.rb +60 -0
- data/lib/rubocop/callbacksystems/project_index/freshness.rb +104 -0
- data/lib/rubocop/callbacksystems/project_index/loader.rb +9 -0
- data/lib/rubocop/callbacksystems/project_index/location.rb +29 -0
- data/lib/rubocop/callbacksystems/project_index/nested_class_confinement.rb +297 -0
- data/lib/rubocop/callbacksystems/project_index/nested_class_method_dispatch.rb +50 -0
- data/lib/rubocop/callbacksystems/project_index/reading.rb +129 -0
- data/lib/rubocop/callbacksystems/project_index/source_file.rb +59 -0
- data/lib/rubocop/callbacksystems/project_index/source_file_match.rb +57 -0
- data/lib/rubocop/callbacksystems/project_index/support.rb +46 -0
- data/lib/rubocop/callbacksystems/source/comment_blocks.rb +43 -0
- data/lib/rubocop/callbacksystems/source/comments.rb +141 -0
- data/lib/rubocop/callbacksystems/source/file_ast.rb +15 -0
- data/lib/rubocop/callbacksystems/source/files_checksum.rb +43 -0
- data/lib/rubocop/callbacksystems/source/single_line_call.rb +181 -0
- data/lib/rubocop/callbacksystems/source/statement_with_comments.rb +131 -0
- data/lib/rubocop/callbacksystems/testing/cop_helpers.rb +42 -0
- data/lib/rubocop/callbacksystems/testing/fixture_call.rb +30 -0
- data/lib/rubocop/callbacksystems/testing/fixture_names.rb +62 -0
- data/lib/rubocop/callbacksystems/testing/path_mapping.rb +104 -0
- data/lib/rubocop/cop/callbacksystems/application_controller_method_definition.rb +44 -0
- data/lib/rubocop/cop/callbacksystems/assert_with_expected_argument.rb +60 -0
- data/lib/rubocop/cop/callbacksystems/base.rb +134 -0
- data/lib/rubocop/cop/callbacksystems/body_order.rb +478 -0
- data/lib/rubocop/cop/callbacksystems/builder_method_parentheses.rb +74 -0
- data/lib/rubocop/cop/callbacksystems/collapse_multiline_expression.rb +140 -0
- data/lib/rubocop/cop/callbacksystems/comment_block_length.rb +137 -0
- data/lib/rubocop/cop/callbacksystems/complex_conditional.rb +75 -0
- data/lib/rubocop/cop/callbacksystems/controller_test_response_assertion.rb +105 -0
- data/lib/rubocop/cop/callbacksystems/controller_test_response_first_assertion.rb +123 -0
- data/lib/rubocop/cop/callbacksystems/data_clump.rb +339 -0
- data/lib/rubocop/cop/callbacksystems/declarative_method_naming.rb +350 -0
- data/lib/rubocop/cop/callbacksystems/delegate_requires_private_option.rb +81 -0
- data/lib/rubocop/cop/callbacksystems/eager_loading_scope_naming.rb +386 -0
- data/lib/rubocop/cop/callbacksystems/early_return.rb +215 -0
- data/lib/rubocop/cop/callbacksystems/empty_line_before_method.rb +168 -0
- data/lib/rubocop/cop/callbacksystems/empty_line_between_sections.rb +169 -0
- data/lib/rubocop/cop/callbacksystems/empty_private_section.rb +107 -0
- data/lib/rubocop/cop/callbacksystems/expanded_empty_class_or_module.rb +71 -0
- data/lib/rubocop/cop/callbacksystems/inline_memoized_computation.rb +110 -0
- data/lib/rubocop/cop/callbacksystems/job_enqueue_methods.rb +93 -0
- data/lib/rubocop/cop/callbacksystems/max_safe_navigation_depth.rb +118 -0
- data/lib/rubocop/cop/callbacksystems/method_invocation_order.rb +261 -0
- data/lib/rubocop/cop/callbacksystems/nested_class_order.rb +255 -0
- data/lib/rubocop/cop/callbacksystems/no_abbreviations.rb +197 -0
- data/lib/rubocop/cop/callbacksystems/no_anemic_delegation.rb +101 -0
- data/lib/rubocop/cop/callbacksystems/no_anemic_record.rb +309 -0
- data/lib/rubocop/cop/callbacksystems/no_assert_select.rb +31 -0
- data/lib/rubocop/cop/callbacksystems/no_bang_method_without_counterpart.rb +451 -0
- data/lib/rubocop/cop/callbacksystems/no_call_method.rb +73 -0
- data/lib/rubocop/cop/callbacksystems/no_comments_in_test_class_body.rb +159 -0
- data/lib/rubocop/cop/callbacksystems/no_consecutive_appends.rb +261 -0
- data/lib/rubocop/cop/callbacksystems/no_custom_route_actions.rb +124 -0
- data/lib/rubocop/cop/callbacksystems/no_direct_eager_loading.rb +90 -0
- data/lib/rubocop/cop/callbacksystems/no_each_with_early_return.rb +58 -0
- data/lib/rubocop/cop/callbacksystems/no_explicit_public_modifier.rb +81 -0
- data/lib/rubocop/cop/callbacksystems/no_hash_with_default_block.rb +68 -0
- data/lib/rubocop/cop/callbacksystems/no_local_variable_return.rb +173 -0
- data/lib/rubocop/cop/callbacksystems/no_manual_accumulation.rb +188 -0
- data/lib/rubocop/cop/callbacksystems/no_mixed_memoization.rb +145 -0
- data/lib/rubocop/cop/callbacksystems/no_nested_modules_in_classes.rb +78 -0
- data/lib/rubocop/cop/callbacksystems/no_nil_at_end_of_method.rb +74 -0
- data/lib/rubocop/cop/callbacksystems/no_redundant_validation_name.rb +37 -0
- data/lib/rubocop/cop/callbacksystems/no_redundant_wrapper_method.rb +161 -0
- data/lib/rubocop/cop/callbacksystems/no_section_divider_comments.rb +105 -0
- data/lib/rubocop/cop/callbacksystems/no_send_in_tests.rb +75 -0
- data/lib/rubocop/cop/callbacksystems/no_service_objects.rb +64 -0
- data/lib/rubocop/cop/callbacksystems/no_single_letter_names.rb +47 -0
- data/lib/rubocop/cop/callbacksystems/no_singleton_class_in_modules.rb +43 -0
- data/lib/rubocop/cop/callbacksystems/no_tap_collection_building.rb +173 -0
- data/lib/rubocop/cop/callbacksystems/no_turbo_stream_in_controllers.rb +52 -0
- data/lib/rubocop/cop/callbacksystems/no_typographic_clutter.rb +159 -0
- data/lib/rubocop/cop/callbacksystems/no_writes_in_get_actions.rb +149 -0
- data/lib/rubocop/cop/callbacksystems/ordered_mixin_arguments.rb +78 -0
- data/lib/rubocop/cop/callbacksystems/plural_controller_names.rb +103 -0
- data/lib/rubocop/cop/callbacksystems/prefer_attr_reader_over_instance_variable.rb +86 -0
- data/lib/rubocop/cop/callbacksystems/prefer_backslash_continuation.rb +478 -0
- data/lib/rubocop/cop/callbacksystems/prefer_class_for_state.rb +532 -0
- data/lib/rubocop/cop/callbacksystems/prefer_class_new_for_empty_nested_class.rb +247 -0
- data/lib/rubocop/cop/callbacksystems/prefer_class_over_builder_block.rb +124 -0
- data/lib/rubocop/cop/callbacksystems/prefer_default_argument.rb +225 -0
- data/lib/rubocop/cop/callbacksystems/prefer_delegate.rb +242 -0
- data/lib/rubocop/cop/callbacksystems/prefer_delegate_over_instance_variable_assignment.rb +175 -0
- data/lib/rubocop/cop/callbacksystems/prefer_excluding.rb +204 -0
- data/lib/rubocop/cop/callbacksystems/prefer_extract_associated.rb +54 -0
- data/lib/rubocop/cop/callbacksystems/prefer_it_block_parameter.rb +156 -0
- data/lib/rubocop/cop/callbacksystems/prefer_many.rb +57 -0
- data/lib/rubocop/cop/callbacksystems/prefer_method_over_lambda.rb +211 -0
- data/lib/rubocop/cop/callbacksystems/prefer_method_over_variable.rb +172 -0
- data/lib/rubocop/cop/callbacksystems/prefer_module_for_static_class.rb +286 -0
- data/lib/rubocop/cop/callbacksystems/prefer_ordinal_array_access.rb +136 -0
- data/lib/rubocop/cop/callbacksystems/prefer_positive_wrap.rb +265 -0
- data/lib/rubocop/cop/callbacksystems/prefer_squish.rb +47 -0
- data/lib/rubocop/cop/callbacksystems/prefer_stringify_keys.rb +24 -0
- data/lib/rubocop/cop/callbacksystems/prefer_symbolize_keys.rb +24 -0
- data/lib/rubocop/cop/callbacksystems/prefer_turbo_page_refresh_broadcasts.rb +40 -0
- data/lib/rubocop/cop/callbacksystems/private_constants_in_private_section.rb +160 -0
- data/lib/rubocop/cop/callbacksystems/private_delegate_placement.rb +342 -0
- data/lib/rubocop/cop/callbacksystems/private_methods_in_private_section.rb +70 -0
- data/lib/rubocop/cop/callbacksystems/private_nested_class_method_visibility.rb +269 -0
- data/lib/rubocop/cop/callbacksystems/private_nested_classes.rb +94 -0
- data/lib/rubocop/cop/callbacksystems/public_methods_must_have_tests.rb +190 -0
- data/lib/rubocop/cop/callbacksystems/redundant_concern_extend.rb +157 -0
- data/lib/rubocop/cop/callbacksystems/redundant_constant_freeze.rb +57 -0
- data/lib/rubocop/cop/callbacksystems/redundant_constant_namespace.rb +134 -0
- data/lib/rubocop/cop/callbacksystems/reflow_comments.rb +133 -0
- data/lib/rubocop/cop/callbacksystems/repeated_fixture_in_tests.rb +106 -0
- data/lib/rubocop/cop/callbacksystems/restricted_controller_actions.rb +85 -0
- data/lib/rubocop/cop/callbacksystems/routes_module_scope.rb +336 -0
- data/lib/rubocop/cop/callbacksystems/single_line_setup_block.rb +107 -0
- data/lib/rubocop/cop/callbacksystems/single_singleton_class.rb +49 -0
- data/lib/rubocop/cop/callbacksystems/single_top_level_class_or_module.rb +160 -0
- data/lib/rubocop/cop/callbacksystems/single_use_fixture_variable.rb +211 -0
- data/lib/rubocop/cop/callbacksystems/single_use_setup_variable.rb +684 -0
- data/lib/rubocop/cop/callbacksystems/singular_scope_names.rb +47 -0
- data/lib/rubocop/cop/callbacksystems/space_inside_percent_literal.rb +73 -0
- data/lib/rubocop/cop/callbacksystems/system_test_browser_driver.rb +30 -0
- data/lib/rubocop/cop/callbacksystems/test_method_definition.rb +86 -0
- data/lib/rubocop/cop/callbacksystems/test_method_order.rb +319 -0
- data/lib/rubocop/cop/callbacksystems/too_many_instance_variables.rb +78 -0
- data/lib/rubocop/cop/callbacksystems/too_many_local_variables.rb +77 -0
- data/lib/rubocop/cop/callbacksystems/too_many_validation_guards.rb +84 -0
- data/lib/rubocop/cop/callbacksystems/unnecessary_local_variable.rb +176 -0
- data/lib/rubocop/cop/callbacksystems/unused_private_method_in_nested_class.rb +203 -0
- data/lib/rubocop-callbacksystems.rb +21 -0
- data/rubocop.yml +685 -0
- metadata +337 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# `delegate` does not inherit the visibility of the surrounding section.
|
|
2
|
+
# A `delegate` placed under a `private` keyword still defines public methods
|
|
3
|
+
# unless `private: true` is passed explicitly.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# # bad - `body` ends up public despite the surrounding `private`
|
|
7
|
+
# class Foo
|
|
8
|
+
# def initialize(node)
|
|
9
|
+
# @node = node
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# private
|
|
13
|
+
# attr_reader :node
|
|
14
|
+
# delegate :body, to: :node
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # good - opts into private visibility explicitly
|
|
18
|
+
# class Foo
|
|
19
|
+
# def initialize(node)
|
|
20
|
+
# @node = node
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# private
|
|
24
|
+
# attr_reader :node
|
|
25
|
+
# delegate :body, to: :node, private: true
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
class RuboCop::Cop::Callbacksystems::DelegateRequiresPrivateOption < RuboCop::Cop::Callbacksystems::Base
|
|
29
|
+
extend RuboCop::Cop::AutoCorrector
|
|
30
|
+
|
|
31
|
+
def on_send(node)
|
|
32
|
+
report DelegateCall.new(node)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
alias on_csend on_send
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
class DelegateCall
|
|
39
|
+
include RuboCop::Callbacksystems::Helpers
|
|
40
|
+
|
|
41
|
+
MESSAGE = "`delegate` under a `private` section must pass `private: true`; visibility is not inherited " \
|
|
42
|
+
"automatically."
|
|
43
|
+
|
|
44
|
+
def initialize(node)
|
|
45
|
+
@node = node
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def offense
|
|
49
|
+
if missing_private_option?
|
|
50
|
+
RuboCop::Callbacksystems::Offense.new(node, MESSAGE, correcting: macro.private_option_known?) { correct(it) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
attr_reader :node
|
|
56
|
+
|
|
57
|
+
def missing_private_option?
|
|
58
|
+
macro.macro? && !macro.private? && inside_private_section?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def macro
|
|
62
|
+
@macro ||= RuboCop::Callbacksystems::Methods::DelegateMacro.new(node)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def inside_private_section?
|
|
66
|
+
enclosing_body && visibility_at(node, enclosing_body) == :private
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def enclosing_body
|
|
70
|
+
@enclosing_body ||= enclosing_body_for(node)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def correct(corrector)
|
|
74
|
+
if macro.private_option
|
|
75
|
+
replace_expression(corrector, macro.private_option.value, with: "true")
|
|
76
|
+
else
|
|
77
|
+
corrector.insert_after(macro.last_option, ", private: true")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Ensures eager loading scopes follow naming conventions. A scope named for the
|
|
2
|
+
# associations it loads (`with_posts`) reads at the call site as what the query
|
|
3
|
+
# carries, while one named for the page that uses it (`with_show_data`) ties the
|
|
4
|
+
# model to a controller, and one named for a generic term (`with_associations`)
|
|
5
|
+
# says nothing about what gets loaded.
|
|
6
|
+
# The `with_*` prefix is reserved for eager loading. A scope whose body only
|
|
7
|
+
# filters or orders records needs a name describing that condition instead.
|
|
8
|
+
# Composed scopes and dynamic query builders stay out when their eager loading
|
|
9
|
+
# cannot be determined from the body.
|
|
10
|
+
# Default scopes may come from ancestors or concerns; unreadable ancestry leaves
|
|
11
|
+
# the reverse naming check undecided while explicit eager loading is still read.
|
|
12
|
+
# - Must use `with_*` prefix for any of includes, preload, or eager_load
|
|
13
|
+
# - Must not contain controller action names (index, show, etc.)
|
|
14
|
+
# - Must not contain generic terms (associations, relations)
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad - missing with_ prefix
|
|
18
|
+
# scope :including_posts, -> { includes(:posts) }
|
|
19
|
+
#
|
|
20
|
+
# # bad - contains controller action
|
|
21
|
+
# scope :with_show_data, -> { includes(:items) }
|
|
22
|
+
#
|
|
23
|
+
# # bad - contains generic term
|
|
24
|
+
# scope :with_associations, -> { includes(:items) }
|
|
25
|
+
#
|
|
26
|
+
# # bad - with_ promises eager loading, not a filter
|
|
27
|
+
# scope :with_active, -> { where(active: true) }
|
|
28
|
+
#
|
|
29
|
+
# # good
|
|
30
|
+
# scope :with_posts, -> { includes(:posts) }
|
|
31
|
+
# scope :with_line_items, -> { includes(:line_items) }
|
|
32
|
+
# scope :active, -> { where(active: true) }
|
|
33
|
+
#
|
|
34
|
+
class RuboCop::Cop::Callbacksystems::EagerLoadingScopeNaming < RuboCop::Cop::Callbacksystems::Base
|
|
35
|
+
include RuboCop::Callbacksystems::ProjectIndex::Support
|
|
36
|
+
|
|
37
|
+
def on_new_investigation
|
|
38
|
+
@default_scopes = if project_index.nil? || project_index_reliable?
|
|
39
|
+
DefaultScopes.new(processed_source, project_index:)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def on_send(node)
|
|
44
|
+
report ScopeName.new(node, default_scopes:) if scope_definition?(node)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias on_csend on_send
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
attr_reader :default_scopes
|
|
51
|
+
|
|
52
|
+
# The name a `scope` declares, read against the association its body eager loads.
|
|
53
|
+
class ScopeName
|
|
54
|
+
include RuboCop::Callbacksystems::Helpers
|
|
55
|
+
|
|
56
|
+
PREFIX_MESSAGE = "Eager loading scopes should follow `with_*` convention. Rename `%<name>s` to " \
|
|
57
|
+
"`with_%<association>s`."
|
|
58
|
+
ACTION_MESSAGE = "Scope name `%<name>s` contains controller action `%<action>s`. Use a more descriptive name."
|
|
59
|
+
GENERIC_MESSAGE = "Scope name `%<name>s` contains generic term `%<term>s`. Use a more specific name like " \
|
|
60
|
+
"`with_%<association>s`."
|
|
61
|
+
RESERVED_PREFIX_MESSAGE = "Reserve `with_*` for eager loading scopes. Rename `%<name>s` to describe its query."
|
|
62
|
+
GENERIC_TERMS = %w[ association associations relation relations ]
|
|
63
|
+
|
|
64
|
+
def initialize(node, default_scopes:)
|
|
65
|
+
@node = node
|
|
66
|
+
@default_scopes = default_scopes
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def offense
|
|
70
|
+
if messages.any?
|
|
71
|
+
RuboCop::Callbacksystems::Offense.new(node.first_argument, messages.join(" "))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
attr_reader :node, :default_scopes
|
|
77
|
+
|
|
78
|
+
def messages
|
|
79
|
+
if association
|
|
80
|
+
[ prefix_message, action_message, generic_message ].compact
|
|
81
|
+
elsif name.start_with?("with_") && scope_body.without_eager_loading?
|
|
82
|
+
[ format(RESERVED_PREFIX_MESSAGE, name:) ]
|
|
83
|
+
else
|
|
84
|
+
[]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def association
|
|
89
|
+
@association ||= scope_body.eager_loading_association
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def scope_body
|
|
93
|
+
@scope_body ||= ScopeBody.new(node.arguments.second, default_scopes:)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def prefix_message
|
|
97
|
+
format(PREFIX_MESSAGE, name:, association:) unless name.start_with?("with_")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def name
|
|
101
|
+
node.first_argument.value.to_s
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def action_message
|
|
105
|
+
format(ACTION_MESSAGE, name:, action: controller_action) if controller_action
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def controller_action
|
|
109
|
+
STANDARD_CONTROLLER_ACTIONS.find { name_parts.include?(it.to_s) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def name_parts
|
|
113
|
+
@name_parts ||= name.split("_")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def generic_message
|
|
117
|
+
format(GENERIC_MESSAGE, name:, term: generic_term, association:) if generic_term
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def generic_term
|
|
121
|
+
GENERIC_TERMS.find { name_parts.include?(it) }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class ScopeBody
|
|
126
|
+
include RuboCop::Callbacksystems::Helpers
|
|
127
|
+
|
|
128
|
+
QUERY_METHODS = %i[
|
|
129
|
+
all distinct group having invert_where joins left_joins left_outer_joins limit none offset order
|
|
130
|
+
readonly references reorder reselect reverse_order rewhere select strict_loading unscope where
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
def initialize(body, default_scopes:)
|
|
134
|
+
@body = body
|
|
135
|
+
@default_scopes = default_scopes
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def eager_loading_association
|
|
139
|
+
association_name_for(eager_load_call) if eager_load_call
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def without_eager_loading?
|
|
143
|
+
deferred_callable_block?(body) && eager_load_call.nil? && !default_scope? && plain_query?(scope_body.body)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
attr_reader :body, :default_scopes
|
|
148
|
+
|
|
149
|
+
def eager_load_call
|
|
150
|
+
return @eager_load_call if defined?(@eager_load_call)
|
|
151
|
+
|
|
152
|
+
@eager_load_call = RuboCop::Callbacksystems::Execution::Immediate.new(scope_body&.body).find do |candidate|
|
|
153
|
+
candidate.call_type? && EAGER_LOADING_METHODS.include?(candidate.method_name)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def scope_body
|
|
158
|
+
body if any_block_type?(body)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def association_name_for(eager_load_node)
|
|
162
|
+
names = eager_load_node.arguments.flat_map { AssociationNames.new(it).to_a }
|
|
163
|
+
names.join("_and_") if names.any?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def default_scope?
|
|
167
|
+
default_scopes.nil? || default_scopes.possible_for?(body)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# A named scope, a merge, or a dynamic builder can carry eager loading from another declaration.
|
|
171
|
+
def plain_query?(expression)
|
|
172
|
+
expression&.call_type? && query_method?(expression) &&
|
|
173
|
+
(call_on_self?(expression) || plain_query?(expression.receiver))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def query_method?(expression)
|
|
177
|
+
QUERY_METHODS.include?(expression.method_name) || where_negation?(expression)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def where_negation?(expression)
|
|
181
|
+
expression.method?(:not) && expression.receiver.call_type? && expression.receiver.method?(:where) &&
|
|
182
|
+
expression.receiver.arguments.empty?
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Absence is trusted only in readable declarations and their known ancestry; a model's defaults may live elsewhere.
|
|
187
|
+
class DefaultScopes
|
|
188
|
+
include RuboCop::Callbacksystems::Helpers
|
|
189
|
+
include RuboCop::Cop::ProjectIndexHelp
|
|
190
|
+
|
|
191
|
+
def initialize(processed_source, project_index:)
|
|
192
|
+
@processed_source = processed_source
|
|
193
|
+
@project_index = project_index
|
|
194
|
+
@sources = {}
|
|
195
|
+
@possibilities = {}.compare_by_identity
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def possible_for?(node)
|
|
199
|
+
owner = enclosing_class_or_module_of(node)
|
|
200
|
+
|
|
201
|
+
owner.nil? || node.each_ancestor.take_while { !it.equal?(owner) }.any? { any_block_type?(it) } ||
|
|
202
|
+
possibilities.fetch(owner) { possibilities[owner] = possible_in?(owner) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
private
|
|
206
|
+
attr_reader :processed_source, :project_index, :sources, :possibilities
|
|
207
|
+
|
|
208
|
+
def possible_in?(owner)
|
|
209
|
+
return true if owner.module_type?
|
|
210
|
+
|
|
211
|
+
declarations = if project_index
|
|
212
|
+
indexed_declarations_for(owner)
|
|
213
|
+
else
|
|
214
|
+
local_declarations_for(owner)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
declarations.nil? || declarations.any? { it.default_scope? || !it.known_ancestry? }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def indexed_declarations_for(owner)
|
|
221
|
+
namespace = resolve_constant_in_index(owner.identifier)
|
|
222
|
+
|
|
223
|
+
if namespace.is_a?(Rubydex::Namespace) && namespace.definitions.any?
|
|
224
|
+
definitions_for(namespace).map { declaration_for(it) }.then do |declarations|
|
|
225
|
+
declarations.presence if declarations.exclude?(nil)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def definitions_for(namespace)
|
|
231
|
+
ancestors = [ *namespace.ancestors, *namespace.singleton_class.ancestors ].map do |ancestor|
|
|
232
|
+
ancestor.is_a?(Rubydex::SingletonClass) ? ancestor.attached_class : ancestor
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
ancestors.uniq(&:name).flat_map { it.definitions.to_a }.select do |definition|
|
|
236
|
+
definition.location.uri.start_with?(FILE_URI_PREFIX)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def declaration_for(definition)
|
|
241
|
+
source_for(definition.location)&.declaration_at(definition.location)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def source_for(location)
|
|
245
|
+
path = location.to_file_path
|
|
246
|
+
|
|
247
|
+
sources.fetch(path) do
|
|
248
|
+
source = if File.expand_path(path) == File.expand_path(processed_source.file_path)
|
|
249
|
+
processed_source
|
|
250
|
+
else
|
|
251
|
+
RuboCop::Callbacksystems::Source::FileAst
|
|
252
|
+
.processed_source(path, ruby_version: processed_source.ruby_version)
|
|
253
|
+
end
|
|
254
|
+
sources[path] = Source.new(source, project_index:) if source
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def local_declarations_for(owner)
|
|
259
|
+
candidates = nodes_in(processed_source.ast, :class, :module).select do |candidate|
|
|
260
|
+
candidate.identifier.short_name == owner.identifier.short_name
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
candidates.map { Declaration.new(it, project_index:) }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
class Source
|
|
267
|
+
include RuboCop::Callbacksystems::Helpers
|
|
268
|
+
|
|
269
|
+
def initialize(processed_source, project_index:)
|
|
270
|
+
@declarations = nodes_in(processed_source.ast, :class, :module).to_h do |node|
|
|
271
|
+
location = RuboCop::Callbacksystems::ProjectIndex::Location.for_ast \
|
|
272
|
+
node.source_range, uri: RuboCop::Callbacksystems::ProjectIndex::SourceFile.new(processed_source.file_path).uri
|
|
273
|
+
[ location, Declaration.new(node, project_index:) ]
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def declaration_at(location)
|
|
278
|
+
declarations[RuboCop::Callbacksystems::ProjectIndex::Location.for_rubydex(location)]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
attr_reader :declarations
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
class Declaration
|
|
286
|
+
include RuboCop::Callbacksystems::Helpers
|
|
287
|
+
|
|
288
|
+
MIXIN_METHODS = %i[ include prepend extend ]
|
|
289
|
+
|
|
290
|
+
def initialize(node, project_index:)
|
|
291
|
+
@node = node
|
|
292
|
+
@project_index = project_index
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def default_scope?
|
|
296
|
+
members.any? do |member|
|
|
297
|
+
member.method?(:default_scope) && (member.any_def_type? || call_on_self?(member))
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def known_ancestry?
|
|
302
|
+
ancestors.all? { Ancestor.new(it, project_index:).known? }
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
private
|
|
306
|
+
attr_reader :node, :project_index
|
|
307
|
+
|
|
308
|
+
def members
|
|
309
|
+
@members ||= nodes_in(node.body, :send, :csend, :def, :defs).select do |member|
|
|
310
|
+
enclosing_class_or_module_of(member).equal?(node)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def ancestors
|
|
315
|
+
superclass = node.parent_class if node.class_type?
|
|
316
|
+
mixins = members.select do |member|
|
|
317
|
+
member.call_type? && call_on_self?(member) && MIXIN_METHODS.include?(member.method_name)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
[ superclass, *mixins.flat_map(&:arguments) ].compact
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
class Ancestor
|
|
325
|
+
include RuboCop::Callbacksystems::Helpers
|
|
326
|
+
include RuboCop::Cop::ProjectIndexHelp
|
|
327
|
+
|
|
328
|
+
FRAMEWORK_ANCESTORS = %w[ ActiveRecord::Base ActiveSupport::Concern ]
|
|
329
|
+
|
|
330
|
+
def initialize(node, project_index:)
|
|
331
|
+
@node = node
|
|
332
|
+
@project_index = project_index
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def known?
|
|
336
|
+
framework? || (project_index && node.const_type? &&
|
|
337
|
+
resolve_constant_in_index(node).is_a?(Rubydex::Namespace))
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
private
|
|
341
|
+
attr_reader :node, :project_index
|
|
342
|
+
|
|
343
|
+
def framework?
|
|
344
|
+
FRAMEWORK_ANCESTORS.include?(constant_name_of(node)&.delete_prefix("::")) &&
|
|
345
|
+
(node.absolute? || lexical_nesting_of(node).empty?)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
class AssociationNames
|
|
351
|
+
include Enumerable
|
|
352
|
+
|
|
353
|
+
def initialize(argument)
|
|
354
|
+
@argument = argument
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def each
|
|
358
|
+
if block_given?
|
|
359
|
+
@pending = [ argument ]
|
|
360
|
+
until pending.empty?
|
|
361
|
+
advance
|
|
362
|
+
visit { yield it }
|
|
363
|
+
end
|
|
364
|
+
else
|
|
365
|
+
to_enum(__method__)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private
|
|
370
|
+
attr_reader :argument, :pending, :current
|
|
371
|
+
|
|
372
|
+
def advance
|
|
373
|
+
@current = pending.pop
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def visit
|
|
377
|
+
if current.type?(:sym, :str)
|
|
378
|
+
yield current.value.to_s
|
|
379
|
+
elsif current.hash_type?
|
|
380
|
+
pending.concat(current.pairs.reverse.map(&:key))
|
|
381
|
+
elsif current.array_type?
|
|
382
|
+
pending.concat(current.children.reverse)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Prohibits early exits except for a single guard clause on the first line.
|
|
2
|
+
# Applies to `return` in methods and `next`/`break` in blocks. A body that exits
|
|
3
|
+
# in several places has to be read with every exit in mind, and the guard at the
|
|
4
|
+
# top is the one shape a reader takes in at a glance. Past it, a condition reads
|
|
5
|
+
# better as a branch wrapping what it guards.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# # bad - return in the middle of method
|
|
9
|
+
# def process(user)
|
|
10
|
+
# data = fetch_data
|
|
11
|
+
# return if data.empty?
|
|
12
|
+
# process_data(data)
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# # bad - multiple early returns
|
|
16
|
+
# def process(user)
|
|
17
|
+
# return unless user
|
|
18
|
+
# return if user.inactive?
|
|
19
|
+
# do_something
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # bad - next in the middle of a block
|
|
23
|
+
# items.each do |item|
|
|
24
|
+
# process(item)
|
|
25
|
+
# next if item.done?
|
|
26
|
+
# finalize(item)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # bad - break in the middle of a block
|
|
30
|
+
# items.each do |item|
|
|
31
|
+
# process(item)
|
|
32
|
+
# break if item.last?
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # good - single guard clause on first line
|
|
36
|
+
# def process(user)
|
|
37
|
+
# return unless user
|
|
38
|
+
# do_something(user)
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# # good - single next guard on first line
|
|
42
|
+
# items.each do |item|
|
|
43
|
+
# next if item.nil?
|
|
44
|
+
# process(item)
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
class RuboCop::Cop::Callbacksystems::EarlyReturn < RuboCop::Cop::Callbacksystems::Base
|
|
48
|
+
def on_def(node)
|
|
49
|
+
report_each IllegalExits.new(node.body, :return) if node.body
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
alias on_defs on_def
|
|
53
|
+
|
|
54
|
+
def on_block(node)
|
|
55
|
+
return if loop_block?(node) || !node.body
|
|
56
|
+
|
|
57
|
+
%i[ next break ].each { report_each IllegalExits.new(node.body, it) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
alias on_numblock on_block
|
|
61
|
+
alias on_itblock on_block
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
# The exits a body takes past the single guard clause it is allowed.
|
|
65
|
+
class IllegalExits
|
|
66
|
+
include RuboCop::Callbacksystems::Helpers
|
|
67
|
+
|
|
68
|
+
MESSAGE = "Avoid early returns. Only a single guard clause on the first line is allowed."
|
|
69
|
+
BLOCK_MESSAGE = "Avoid early next/break. Only a single guard clause on the first line is allowed."
|
|
70
|
+
|
|
71
|
+
def initialize(body, exit_type)
|
|
72
|
+
@body = body
|
|
73
|
+
@exit_type = exit_type
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def each_offense
|
|
77
|
+
exits.each { yield RuboCop::Callbacksystems::Offense.new(it, message) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
attr_reader :body, :exit_type
|
|
82
|
+
|
|
83
|
+
def exits
|
|
84
|
+
BodyExits.new(body, exit_type, walk_blocks: returning?)
|
|
85
|
+
.reject { it.equal?(allowed_exit) || handled_by_each_search?(it) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A method's `return` leaves the blocks written inside it, while a block's `next` or `break` stays in its own.
|
|
89
|
+
def returning?
|
|
90
|
+
exit_type == :return
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def allowed_exit
|
|
94
|
+
@allowed_exit ||= Guard.new(body, exit_type).allowed_exit
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def handled_by_each_search?(exit)
|
|
98
|
+
returning? && exit.each_ancestor(:any_block).any? do |block|
|
|
99
|
+
block.method?(:each) && block.body&.source_range&.contains?(exit.source_range) &&
|
|
100
|
+
conditional_return_of_block_argument?(exit, block: block)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def message
|
|
105
|
+
returning? ? MESSAGE : BLOCK_MESSAGE
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The exits of one node, gathered branch by branch through the nodes written inside it.
|
|
110
|
+
class BodyExits
|
|
111
|
+
include Enumerable
|
|
112
|
+
include RuboCop::Callbacksystems::Helpers
|
|
113
|
+
|
|
114
|
+
def initialize(node, target_type, walk_blocks:)
|
|
115
|
+
@node = node
|
|
116
|
+
@target_type = target_type
|
|
117
|
+
@walk_blocks = walk_blocks
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def each
|
|
121
|
+
if block_given?
|
|
122
|
+
@pending = [ node ].compact
|
|
123
|
+
until pending.empty?
|
|
124
|
+
advance
|
|
125
|
+
visit { yield it }
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
to_enum(__method__)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
attr_reader :node, :target_type, :walk_blocks, :pending, :current
|
|
134
|
+
|
|
135
|
+
def advance
|
|
136
|
+
@current = pending.pop
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def visit
|
|
140
|
+
if current.type == target_type
|
|
141
|
+
yield current
|
|
142
|
+
else
|
|
143
|
+
pending.concat(children_to_walk.reverse)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def children_to_walk
|
|
148
|
+
if any_block_type?(current)
|
|
149
|
+
block_children
|
|
150
|
+
elsif lexical_definition?(current)
|
|
151
|
+
immediate_inputs_of(current)
|
|
152
|
+
elsif nested_iteration?
|
|
153
|
+
nested_iteration_inputs
|
|
154
|
+
else
|
|
155
|
+
current.each_child_node.to_a
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# A block's receiver and arguments run in the surrounding scope even when its body is a deferred callable.
|
|
160
|
+
def block_children
|
|
161
|
+
immediate_inputs_of(current) + walked_block_body
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def walked_block_body
|
|
165
|
+
walkable_block? ? [ current.body ] : []
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def walkable_block?
|
|
169
|
+
walk_blocks && !deferred_callable_block?(current) && !method_definition_block?(current) &&
|
|
170
|
+
!loop_block?(current) && current.body
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# A `next` or `break` inside a language loop belongs to that loop rather than the surrounding block.
|
|
174
|
+
def nested_iteration?
|
|
175
|
+
!walk_blocks && current.type?(:while, :while_post, :until, :until_post, :for)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# A `for` collection runs before that loop owns `next` and `break`; its body runs after.
|
|
179
|
+
def nested_iteration_inputs
|
|
180
|
+
current.for_type? ? [ current.collection ] : []
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class Guard
|
|
185
|
+
include RuboCop::Callbacksystems::Helpers
|
|
186
|
+
|
|
187
|
+
def initialize(body, exit_type)
|
|
188
|
+
@body = body
|
|
189
|
+
@exit_type = exit_type
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def allowed_exit
|
|
193
|
+
if_guard || unless_guard if first_statement&.if_type?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
attr_reader :body, :exit_type
|
|
198
|
+
|
|
199
|
+
def first_statement
|
|
200
|
+
@first_statement ||= first_statement_in(body)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def if_guard
|
|
204
|
+
first_statement.if_branch if exit?(first_statement.if_branch) && first_statement.else_branch.nil?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def exit?(branch)
|
|
208
|
+
branch&.type == exit_type
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def unless_guard
|
|
212
|
+
first_statement.else_branch if exit?(first_statement.else_branch) && first_statement.if_branch.nil?
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|