Skip to content

Commit f7d93e5

Browse files
committed
[Fix #12309] Add new Style/ZeroAritySuper cop
1 parent a909dda commit f7d93e5

File tree

5 files changed

+371
-0
lines changed

5 files changed

+371
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#12309](https://github.com/rubocop/rubocop/issues/12309): Add new `Style/SuperArguments` cop. ([@earlopain][])

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5424,6 +5424,11 @@ Style/StructInheritance:
54245424
VersionAdded: '0.29'
54255425
VersionChanged: '1.20'
54265426

5427+
Style/SuperArguments:
5428+
Description: 'Call `super` without arguments and parentheses when the signature is identical.'
5429+
Enabled: pending
5430+
VersionAdded: '<<next>>'
5431+
54275432
Style/SuperWithArgsParentheses:
54285433
Description: 'Use parentheses for `super` with arguments.'
54295434
StyleGuide: '#super-with-args'

lib/rubocop.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@
683683
require_relative 'rubocop/cop/style/string_methods'
684684
require_relative 'rubocop/cop/style/strip'
685685
require_relative 'rubocop/cop/style/struct_inheritance'
686+
require_relative 'rubocop/cop/style/super_arguments'
686687
require_relative 'rubocop/cop/style/super_with_args_parentheses'
687688
require_relative 'rubocop/cop/style/swap_values'
688689
require_relative 'rubocop/cop/style/symbol_array'
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Style
6+
# Checks for redundant argument forwarding when calling super
7+
# with arguments identical to the method definition.
8+
#
9+
# @example
10+
# # bad
11+
# def method(*args, **kwargs)
12+
# super(*args, **kwargs)
13+
# end
14+
#
15+
# # good - implicitly passing all arguments
16+
# def method(*args, **kwargs)
17+
# super
18+
# end
19+
#
20+
# # good - forwarding a subset of the arguments
21+
# def method(*args, **kwargs)
22+
# super(*args)
23+
# end
24+
#
25+
# # good - forwarding no arguments
26+
# def method(*args, **kwargs)
27+
# super()
28+
# end
29+
class SuperArguments < Base
30+
extend AutoCorrector
31+
32+
DEF_TYPES = %i[def defs].freeze
33+
34+
MSG = 'Call `super` without arguments and parentheses when the signature is identical.'
35+
36+
def on_super(super_node)
37+
def_node = super_node.ancestors.find do |node|
38+
# You can't implicitly call super when dynamically defining methods
39+
break if define_method?(node)
40+
41+
break node if DEF_TYPES.include?(node.type)
42+
end
43+
return unless def_node
44+
return unless arguments_identical?(def_node.arguments.argument_list, super_node.arguments)
45+
46+
add_offense(super_node) { |corrector| corrector.replace(super_node, 'super') }
47+
end
48+
49+
private
50+
51+
# rubocop:disable Metrics/CyclomaticComplexity
52+
# rubocop:disable Metrics/PerceivedComplexity
53+
def arguments_identical?(def_args, super_args)
54+
super_args = preprocess_super_args(super_args)
55+
return false if def_args.size != super_args.size
56+
57+
def_args.zip(super_args).each do |def_arg, super_arg|
58+
next if positional_arg_same?(def_arg, super_arg)
59+
next if positional_rest_arg_same(def_arg, super_arg)
60+
next if keyword_arg_same?(def_arg, super_arg)
61+
next if keyword_rest_arg_same?(def_arg, super_arg)
62+
next if block_arg_same?(def_arg, super_arg)
63+
next if forward_arg_same?(def_arg, super_arg)
64+
65+
return false
66+
end
67+
true
68+
end
69+
# rubocop:enable Metrics/CyclomaticComplexity
70+
# rubocop:enable Metrics/PerceivedComplexity
71+
72+
def positional_arg_same?(def_arg, super_arg)
73+
return false unless def_arg.arg_type? || def_arg.optarg_type?
74+
return false unless super_arg.lvar_type?
75+
76+
def_arg.name == super_arg.children.first
77+
end
78+
79+
def positional_rest_arg_same(def_arg, super_arg)
80+
return false unless def_arg.restarg_type?
81+
# anon forwarding
82+
return true if def_arg.name.nil? && super_arg.forwarded_restarg_type?
83+
return false unless super_arg.splat_type?
84+
return false unless (lvar_node = super_arg.children.first).lvar_type?
85+
86+
def_arg.name == lvar_node.children.first
87+
end
88+
89+
def keyword_arg_same?(def_arg, super_arg)
90+
return false unless def_arg.kwarg_type? || def_arg.kwoptarg_type?
91+
return false unless (pair_node = super_arg).pair_type?
92+
return false unless (sym_node = pair_node.key).sym_type?
93+
return false unless (lvar_node = pair_node.value).lvar_type?
94+
return false unless sym_node.value == lvar_node.children.first
95+
96+
def_arg.name == sym_node.value
97+
end
98+
99+
def keyword_rest_arg_same?(def_arg, super_arg)
100+
return false unless def_arg.kwrestarg_type?
101+
# anon forwarding
102+
return true if def_arg.name.nil? && super_arg.forwarded_kwrestarg_type?
103+
return false unless super_arg.kwsplat_type?
104+
return false unless (lvar_node = super_arg.children.first).lvar_type?
105+
106+
def_arg.name == lvar_node.children.first
107+
end
108+
109+
def block_arg_same?(def_arg, super_arg)
110+
return false unless def_arg.blockarg_type? && super_arg.block_pass_type?
111+
# anon forwarding
112+
return true if (block_pass_child = super_arg.children.first).nil? && def_arg.name.nil?
113+
114+
def_arg.name == block_pass_child.children.first
115+
end
116+
117+
def forward_arg_same?(def_arg, super_arg)
118+
return false unless def_arg.forward_arg_type? && super_arg.forwarded_args_type?
119+
120+
true
121+
end
122+
123+
def define_method?(node)
124+
return false unless node.block_type?
125+
126+
child = node.child_nodes.first
127+
return false unless child.send_type?
128+
129+
child.method?(:define_method) || child.method?(:define_singleton_method)
130+
end
131+
132+
def preprocess_super_args(super_args)
133+
super_args.map do |node|
134+
if node.hash_type? && !node.braces?
135+
node.children
136+
else
137+
node
138+
end
139+
end.flatten
140+
end
141+
end
142+
end
143+
end
144+
end
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Style::SuperArguments, :config do
4+
shared_examples 'offense' do |description, args, forwarded_args = args|
5+
it "registers and corrects an offense when using def`#{description} (#{args}) => (#{forwarded_args})`" do
6+
expect_offense(<<~RUBY, forwarded_args: forwarded_args)
7+
def method(#{args})
8+
super(#{forwarded_args})
9+
^^^^^^^{forwarded_args}^ Call `super` without arguments and parentheses when the signature is identical.
10+
end
11+
RUBY
12+
13+
expect_correction(<<~RUBY)
14+
def method(#{args})
15+
super
16+
end
17+
RUBY
18+
end
19+
20+
it "registers and corrects an offense when using defs`#{description} (#{args}) => (#{forwarded_args})`" do
21+
expect_offense(<<~RUBY, forwarded_args: forwarded_args)
22+
def self.method(#{args})
23+
super(#{forwarded_args})
24+
^^^^^^^{forwarded_args}^ Call `super` without arguments and parentheses when the signature is identical.
25+
end
26+
RUBY
27+
28+
expect_correction(<<~RUBY)
29+
def self.method(#{args})
30+
super
31+
end
32+
RUBY
33+
end
34+
end
35+
36+
shared_examples 'no offense' do |description, args, forwarded_args = args|
37+
it "registers no offense when using def `#{description} (#{args}) => (#{forwarded_args})`" do
38+
expect_no_offenses(<<~RUBY)
39+
def method(#{args})
40+
super(#{forwarded_args})
41+
end
42+
RUBY
43+
end
44+
45+
it "registers no offense when using defs `#{description} (#{args}) => (#{forwarded_args})`" do
46+
expect_no_offenses(<<~RUBY)
47+
def self.method(#{args})
48+
super(#{forwarded_args})
49+
end
50+
RUBY
51+
end
52+
end
53+
54+
it_behaves_like 'offense', 'no arguments', ''
55+
it_behaves_like 'offense', 'single positional argument', 'a'
56+
it_behaves_like 'offense', 'multiple positional arguments', 'a, b'
57+
it_behaves_like 'offense', 'multiple positional arguments with default', 'a, b, c = 1', 'a, b, c'
58+
it_behaves_like 'offense', 'positional/keyword argument', 'a, b:', 'a, b: b'
59+
it_behaves_like 'offense', 'positional/keyword argument with default', 'a, b: 1', 'a, b: b'
60+
it_behaves_like 'offense', 'positional/keyword argument both with default', 'a = 1, b: 2', 'a, b: b'
61+
it_behaves_like 'offense', 'named block argument', '&blk'
62+
it_behaves_like 'offense', 'positional splat arguments', '*args'
63+
it_behaves_like 'offense', 'keyword splat arguments', '**kwargs'
64+
it_behaves_like 'offense', 'positional/keyword splat arguments', '*args, **kwargs'
65+
it_behaves_like 'offense', 'positionalkeyword splat arguments with block', '*args, **kwargs, &blk'
66+
it_behaves_like 'offense', 'keyword arguments mixed with forwarding', 'a:, **kwargs', 'a: a, **kwargs'
67+
it_behaves_like 'offense', 'tripple dot forwarding', '...'
68+
it_behaves_like 'offense', 'tripple dot forwarding with extra arg', 'a, ...'
69+
70+
it_behaves_like 'no offense', 'different amount of positional arguments', 'a, b', 'a'
71+
it_behaves_like 'no offense', 'positional arguments in different order', 'a, b', 'b, a'
72+
it_behaves_like 'no offense', 'keyword arguments in different order', 'a:, b:', 'b: b, a: a'
73+
it_behaves_like 'no offense', 'positional/keyword argument mixing', 'a, b', 'a, b: b'
74+
it_behaves_like 'no offense', 'positional/keyword argument mixing reversed', 'a, b:', 'a, b'
75+
it_behaves_like 'no offense', 'block argument with different name', '&blk', '&other_blk'
76+
it_behaves_like 'no offense', 'keyword arguments and hash', 'a:', '{ a: a }'
77+
it_behaves_like 'no offense', 'keyword arguments with send node', 'a:, b:', 'a: a, b: c'
78+
it_behaves_like 'no offense', 'tripple dot forwarding with extra param', '...', 'a, ...'
79+
it_behaves_like 'no offense', 'tripple dot forwarding with different param', 'a, ...', 'b, ...'
80+
it_behaves_like 'no offense', 'keyword forwarding with extra keyword', 'a, **kwargs', 'a: a, **kwargs'
81+
82+
context 'Ruby >= 3.1', :ruby31 do
83+
it_behaves_like 'offense', 'hash value omission', 'a:'
84+
it_behaves_like 'offense', 'anon block forwarding', '&'
85+
end
86+
87+
context 'Ruby >= 3.2', :ruby32 do
88+
it_behaves_like 'offense', 'anon positional forwarding', '*'
89+
it_behaves_like 'offense', 'anon keyword forwarding', '**'
90+
91+
it_behaves_like 'no offense', 'mixed anon forwarding', '*, **', '*'
92+
it_behaves_like 'no offense', 'mixed anon forwarding', '*, **', '**'
93+
end
94+
95+
it 'registers no offense when explicitly passing no arguments' do
96+
expect_no_offenses(<<~RUBY)
97+
def foo(a)
98+
super()
99+
end
100+
RUBY
101+
end
102+
103+
it 'registers an offense when passign along no arguments' do
104+
expect_offense(<<~RUBY)
105+
def foo
106+
super()
107+
^^^^^^^ Call `super` without arguments and parentheses when the signature is identical.
108+
end
109+
RUBY
110+
end
111+
112+
it 'registers an offense for nested declarations' do
113+
expect_offense(<<~RUBY)
114+
def foo(a)
115+
def bar(b:)
116+
super(b: b)
117+
^^^^^^^^^^^ Call `super` without arguments and parentheses when the signature is identical.
118+
end
119+
super(a)
120+
^^^^^^^^ Call `super` without arguments and parentheses when the signature is identical.
121+
end
122+
RUBY
123+
end
124+
125+
it 'registers no offense when calling super in a dsl method' do
126+
expect_no_offenses(<<~RUBY)
127+
describe 'example' do
128+
subject { super() }
129+
end
130+
RUBY
131+
end
132+
133+
context 'when calling super with an extra block argument' do
134+
it 'registers no offense when calling super with no arguments' do
135+
expect_no_offenses(<<~RUBY)
136+
def test
137+
super { x }
138+
end
139+
RUBY
140+
end
141+
142+
it 'registers no offense when calling super with implicit positional arguments' do
143+
expect_no_offenses(<<~RUBY)
144+
def test(a)
145+
super { x }
146+
end
147+
RUBY
148+
end
149+
150+
it 'registers no offense for a method with block when calling super with positional argument' do
151+
expect_no_offenses(<<~RUBY)
152+
def test(a, &blk)
153+
super(a) { x }
154+
end
155+
RUBY
156+
end
157+
end
158+
159+
context 'scope changes' do
160+
it 'registers no offense when the scope changes because of a class definition with block' do
161+
expect_no_offenses(<<~RUBY)
162+
def foo(a)
163+
Class.new do
164+
def foo(a, b)
165+
super(a)
166+
end
167+
end
168+
end
169+
RUBY
170+
end
171+
end
172+
173+
it 'registers an offense when the scope changes because of a block' do
174+
expect_offense(<<~RUBY)
175+
def foo(a)
176+
bar do
177+
super(a)
178+
^^^^^^^^ Call `super` without arguments and parentheses when the signature is identical.
179+
end
180+
end
181+
RUBY
182+
end
183+
184+
it 'registers an offense when the scope changes because of a numblock' do
185+
expect_offense(<<~RUBY)
186+
def foo(a)
187+
bar do
188+
baz(_1)
189+
super(a)
190+
^^^^^^^^ Call `super` without arguments and parentheses when the signature is identical.
191+
end
192+
end
193+
RUBY
194+
end
195+
196+
it 'registers no offense when the scope changes because of sclass' do
197+
expect_no_offenses(<<~RUBY)
198+
def foo(a)
199+
class << self
200+
def foo(b)
201+
super(a)
202+
end
203+
end
204+
end
205+
RUBY
206+
end
207+
208+
it 'registers no offense when calling super in define_singleton_method' do
209+
expect_no_offenses(<<~RUBY)
210+
def test(a)
211+
define_singleton_method(:test2) do |a|
212+
super(a)
213+
end
214+
b.define_singleton_method(:test2) do |a|
215+
super(a)
216+
end
217+
end
218+
RUBY
219+
end
220+
end

0 commit comments

Comments
 (0)