Skip to content

Commit 37e9e5f

Browse files
dvandersluisbbatsov
authored andcommitted
[Fix #12140] Add new Style/CombinableDefined cop.
1 parent f8aa27f commit 37e9e5f

File tree

5 files changed

+326
-0
lines changed

5 files changed

+326
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#12140](https://github.com/rubocop/rubocop/issues/12140): Add new `Style/CombinableDefined` cop. ([@dvandersluis][])

config/default.yml

+5
Original file line numberDiff line numberDiff line change
@@ -3553,6 +3553,11 @@ Style/ColonMethodDefinition:
35533553
Enabled: true
35543554
VersionAdded: '0.52'
35553555

3556+
Style/CombinableDefined:
3557+
Description: 'Checks successive `defined?` calls that can be combined into a single call.'
3558+
Enabled: pending
3559+
VersionAdded: '<<next>>'
3560+
35563561
Style/CombinableLoops:
35573562
Description: >-
35583563
Checks for places where multiple consecutive loops over the same data

lib/rubocop.rb

+1
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@
492492
require_relative 'rubocop/cop/style/collection_methods'
493493
require_relative 'rubocop/cop/style/colon_method_call'
494494
require_relative 'rubocop/cop/style/colon_method_definition'
495+
require_relative 'rubocop/cop/style/combinable_defined'
495496
require_relative 'rubocop/cop/style/combinable_loops'
496497
require_relative 'rubocop/cop/style/command_literal'
497498
require_relative 'rubocop/cop/style/comment_annotation'
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Style
6+
# Checks for multiple `defined?` calls joined by `&&` that can be combined
7+
# into a single `defined?`.
8+
#
9+
# When checking that a nested constant or chained method is defined, it is
10+
# not necessary to check each ancestor or component of the chain.
11+
#
12+
# @example
13+
# # bad
14+
# defined?(Foo) && defined?(Foo::Bar) && defined?(Foo::Bar::Baz)
15+
#
16+
# # good
17+
# defined?(Foo::Bar::Baz)
18+
#
19+
# # bad
20+
# defined?(foo) && defined?(foo.bar) && defined?(foo.bar.baz)
21+
#
22+
# # good
23+
# defined?(foo.bar.baz)
24+
class CombinableDefined < Base
25+
extend AutoCorrector
26+
include RangeHelp
27+
28+
MSG = 'Combine nested `defined?` calls.'
29+
OPERATORS = %w[&& and].freeze
30+
31+
def on_and(node)
32+
# Only register an offense if all `&&` terms are `defined?` calls
33+
return unless (terms = terms(node)).all?(&:defined_type?)
34+
35+
calls = defined_calls(terms)
36+
namespaces = namespaces(calls)
37+
38+
calls.each do |call|
39+
next unless namespaces.any?(call)
40+
41+
add_offense(node) do |corrector|
42+
remove_term(corrector, call)
43+
end
44+
end
45+
end
46+
47+
private
48+
49+
def terms(node)
50+
node.each_descendant.select do |descendant|
51+
descendant.parent.and_type? && !descendant.and_type?
52+
end
53+
end
54+
55+
def defined_calls(nodes)
56+
nodes.filter_map do |defined_node|
57+
subject = defined_node.first_argument
58+
subject if subject.const_type? || subject.call_type?
59+
end
60+
end
61+
62+
def namespaces(nodes)
63+
nodes.filter_map do |node|
64+
if node.respond_to?(:namespace)
65+
node.namespace
66+
elsif node.respond_to?(:receiver)
67+
node.receiver
68+
end
69+
end
70+
end
71+
72+
def remove_term(corrector, term)
73+
term = term.parent until term.parent.and_type?
74+
range = if term == term.parent.children.last
75+
rhs_range_to_remove(term)
76+
else
77+
lhs_range_to_remove(term)
78+
end
79+
80+
corrector.remove(range)
81+
end
82+
83+
# If the redundant `defined?` node is the LHS of an `and` node,
84+
# the term as well as the subsequent `&&`/`and` operator will be removed.
85+
def lhs_range_to_remove(term)
86+
source = @processed_source.buffer.source
87+
88+
pos = term.source_range.end_pos
89+
pos += 1 until source[..pos].end_with?(*OPERATORS)
90+
91+
range_with_surrounding_space(
92+
range: term.source_range.with(end_pos: pos + 1),
93+
side: :right,
94+
newlines: false
95+
)
96+
end
97+
98+
# If the redundant `defined?` node is the RHS of an `and` node,
99+
# the term as well as the preceding `&&`/`and` operator will be removed.
100+
def rhs_range_to_remove(term)
101+
source = @processed_source.buffer.source
102+
103+
pos = term.source_range.begin_pos
104+
pos -= 1 until source[pos, 3].start_with?(*OPERATORS)
105+
106+
range_with_surrounding_space(
107+
range: term.source_range.with(begin_pos: pos - 1),
108+
side: :right,
109+
newlines: false
110+
)
111+
end
112+
end
113+
end
114+
end
115+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Style::CombinableDefined, :config do
4+
it 'does not register an offense for a single `defined?`' do
5+
expect_no_offenses(<<~RUBY)
6+
defined?(Foo)
7+
RUBY
8+
end
9+
10+
%i[&& and].each do |operator|
11+
context "joined by `#{operator}`" do
12+
it 'does not register an offense for two separate `defined?`s' do
13+
expect_no_offenses(<<~RUBY)
14+
defined?(Foo) #{operator} defined?(Bar)
15+
RUBY
16+
end
17+
18+
it 'does not register an offense for two identical `defined?`s' do
19+
# handled by Lint/BinaryOperatorWithIdenticalOperands
20+
expect_no_offenses(<<~RUBY)
21+
defined?(Foo) #{operator} defined?(Foo)
22+
RUBY
23+
end
24+
25+
it 'does not register an offense for the same constant with different `cbase`s' do
26+
expect_no_offenses(<<~RUBY)
27+
defined?(Foo) #{operator} defined?(::Foo)
28+
RUBY
29+
end
30+
31+
it 'registers an offense for two `defined?`s with same nesting' do
32+
expect_offense(<<~RUBY, operator: operator)
33+
defined?(Foo) #{operator} defined?(Foo::Bar)
34+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
35+
RUBY
36+
37+
expect_correction(<<~RUBY)
38+
defined?(Foo::Bar)
39+
RUBY
40+
end
41+
42+
it 'registers an offense for two `defined?`s with the same nesting in reverse order' do
43+
expect_offense(<<~RUBY, operator: operator)
44+
defined?(Foo::Bar) #{operator} defined?(Foo)
45+
^^^^^^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^ Combine nested `defined?` calls.
46+
RUBY
47+
48+
expect_correction(<<~RUBY)
49+
defined?(Foo::Bar)
50+
RUBY
51+
end
52+
53+
it 'registers an offense for two `defined?`s with the same nesting and `cbase`' do
54+
expect_offense(<<~RUBY, operator: operator)
55+
defined?(::Foo) #{operator} defined?(::Foo::Bar)
56+
^^^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
57+
RUBY
58+
59+
expect_correction(<<~RUBY)
60+
defined?(::Foo::Bar)
61+
RUBY
62+
end
63+
64+
it 'registers an offense for two `defined?`s with the same deep nesting' do
65+
expect_offense(<<~RUBY, operator: operator)
66+
defined?(Foo::Bar) #{operator} defined?(Foo::Bar::Baz)
67+
^^^^^^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
68+
RUBY
69+
70+
expect_correction(<<~RUBY)
71+
defined?(Foo::Bar::Baz)
72+
RUBY
73+
end
74+
75+
it 'registers an offense for three `defined?`s with same nesting' do
76+
expect_offense(<<~RUBY, operator: operator)
77+
defined?(Foo) #{operator} defined?(Foo::Bar) #{operator} defined?(Foo::Bar::Baz)
78+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
79+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
80+
RUBY
81+
82+
expect_correction(<<~RUBY)
83+
defined?(Foo::Bar::Baz)
84+
RUBY
85+
end
86+
87+
it 'registers an offense for three `defined?`s with the same module ancestor' do
88+
expect_offense(<<~RUBY, operator: operator)
89+
defined?(Foo) #{operator} defined?(Foo::Bar) #{operator} defined?(Foo::Baz)
90+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
91+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
92+
RUBY
93+
94+
expect_correction(<<~RUBY)
95+
defined?(Foo::Bar) #{operator} defined?(Foo::Baz)
96+
RUBY
97+
end
98+
99+
it 'does not register an offense for two `defined?`s with same namespace but different nesting' do
100+
expect_no_offenses(<<~RUBY)
101+
defined?(Foo::Bar) #{operator} defined?(Foo::Baz)
102+
RUBY
103+
end
104+
105+
it 'does not register an offense for two `defined?`s with negation' do
106+
expect_no_offenses(<<~RUBY)
107+
defined?(Foo) #{operator} !defined?(Foo::Bar)
108+
RUBY
109+
end
110+
111+
it 'does not register an offense for two `defined?` with different `cbase`s' do
112+
expect_no_offenses(<<~RUBY)
113+
defined?(::Foo) #{operator} defined?(Foo::Bar)
114+
RUBY
115+
end
116+
117+
it 'does not register an offense when skipping a nesting level' do
118+
expect_no_offenses(<<~RUBY)
119+
defined?(Foo) #{operator} defined?(Foo::Bar::Baz)
120+
RUBY
121+
end
122+
123+
it 'registers an offense when the namespace is not a constant' do
124+
expect_offense(<<~RUBY, operator: operator)
125+
defined?(foo) #{operator} defined?(foo::Bar)
126+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
127+
RUBY
128+
129+
expect_correction(<<~RUBY)
130+
defined?(foo::Bar)
131+
RUBY
132+
end
133+
134+
it 'registers an offense for method chain with dots' do
135+
expect_offense(<<~RUBY, operator: operator)
136+
defined?(foo) #{operator} defined?(foo.bar)
137+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
138+
RUBY
139+
140+
expect_correction(<<~RUBY)
141+
defined?(foo.bar)
142+
RUBY
143+
end
144+
145+
it 'registers an offense for method chain with `::`' do
146+
expect_offense(<<~RUBY, operator: operator)
147+
defined?(foo) #{operator} defined?(foo::bar)
148+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
149+
RUBY
150+
151+
expect_correction(<<~RUBY)
152+
defined?(foo::bar)
153+
RUBY
154+
end
155+
156+
it 'registers an offense for a method chain followed by constant nesting' do
157+
expect_offense(<<~RUBY, operator: operator)
158+
defined?(foo) #{operator} defined?(foo.bar) #{operator} defined?(foo.bar::BAZ)
159+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
160+
^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^^{operator}^^^^^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
161+
RUBY
162+
163+
expect_correction(<<~RUBY)
164+
defined?(foo.bar::BAZ)
165+
RUBY
166+
end
167+
168+
it 'does not register an offense when there is another term between `defined?`s' do
169+
expect_no_offenses(<<~RUBY)
170+
foo #{operator} defined?(Foo) #{operator} bar #{operator} defined?(Foo::Bar)
171+
RUBY
172+
end
173+
end
174+
end
175+
176+
context 'mixed operators' do
177+
it 'registers an offense and corrects' do
178+
expect_offense(<<~RUBY)
179+
defined?(Foo) && defined?(Foo::Bar) and defined?(Foo::Bar::Baz)
180+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
181+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
182+
RUBY
183+
184+
expect_correction(<<~RUBY)
185+
defined?(Foo::Bar::Baz)
186+
RUBY
187+
end
188+
189+
it 'registers an offense and corrects when an operator is retained' do
190+
expect_offense(<<~RUBY)
191+
defined?(Foo) && defined?(Foo::Bar) and defined?(Foo::Baz)
192+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
193+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Combine nested `defined?` calls.
194+
RUBY
195+
196+
# The deleted operator is the one attached to the term being removed
197+
# (in this case `defined?(Foo)`).
198+
# `Style/AndOr` will subsequently update the operator if necessary.
199+
expect_correction(<<~RUBY)
200+
defined?(Foo::Bar) and defined?(Foo::Baz)
201+
RUBY
202+
end
203+
end
204+
end

0 commit comments

Comments
 (0)