Skip to content

Commit 3227bf0

Browse files
Earlopainbbatsov
authored andcommitted
[Fix #12309] Add new Style/SuperArguments cop
1 parent 9e78c46 commit 3227bf0

File tree

5 files changed

+364
-0
lines changed

5 files changed

+364
-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
@@ -5425,6 +5425,11 @@ Style/StructInheritance:
54255425
VersionAdded: '0.29'
54265426
VersionChanged: '1.20'
54275427

5428+
Style/SuperArguments:
5429+
Description: 'Call `super` without arguments and parentheses when the signature is identical.'
5430+
Enabled: pending
5431+
VersionAdded: '<<next>>'
5432+
54285433
Style/SuperWithArgsParentheses:
54295434
Description: 'Use parentheses for `super` with arguments.'
54305435
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: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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, Metrics/PerceivedComplexity
52+
def arguments_identical?(def_args, super_args)
53+
super_args = preprocess_super_args(super_args)
54+
return false if def_args.size != super_args.size
55+
56+
def_args.zip(super_args).each do |def_arg, super_arg|
57+
next if positional_arg_same?(def_arg, super_arg)
58+
next if positional_rest_arg_same(def_arg, super_arg)
59+
next if keyword_arg_same?(def_arg, super_arg)
60+
next if keyword_rest_arg_same?(def_arg, super_arg)
61+
next if block_arg_same?(def_arg, super_arg)
62+
next if forward_arg_same?(def_arg, super_arg)
63+
64+
return false
65+
end
66+
true
67+
end
68+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
69+
70+
def positional_arg_same?(def_arg, super_arg)
71+
return false unless def_arg.arg_type? || def_arg.optarg_type?
72+
return false unless super_arg.lvar_type?
73+
74+
def_arg.name == super_arg.children.first
75+
end
76+
77+
def positional_rest_arg_same(def_arg, super_arg)
78+
return false unless def_arg.restarg_type?
79+
# anonymous forwarding
80+
return true if def_arg.name.nil? && super_arg.forwarded_restarg_type?
81+
return false unless super_arg.splat_type?
82+
return false unless (lvar_node = super_arg.children.first).lvar_type?
83+
84+
def_arg.name == lvar_node.children.first
85+
end
86+
87+
def keyword_arg_same?(def_arg, super_arg)
88+
return false unless def_arg.kwarg_type? || def_arg.kwoptarg_type?
89+
return false unless (pair_node = super_arg).pair_type?
90+
return false unless (sym_node = pair_node.key).sym_type?
91+
return false unless (lvar_node = pair_node.value).lvar_type?
92+
return false unless sym_node.source == lvar_node.source
93+
94+
def_arg.name == sym_node.value
95+
end
96+
97+
def keyword_rest_arg_same?(def_arg, super_arg)
98+
return false unless def_arg.kwrestarg_type?
99+
# anonymous forwarding
100+
return true if def_arg.name.nil? && super_arg.forwarded_kwrestarg_type?
101+
return false unless super_arg.kwsplat_type?
102+
return false unless (lvar_node = super_arg.children.first).lvar_type?
103+
104+
def_arg.name == lvar_node.children.first
105+
end
106+
107+
def block_arg_same?(def_arg, super_arg)
108+
return false unless def_arg.blockarg_type? && super_arg.block_pass_type?
109+
# anonymous forwarding
110+
return true if (block_pass_child = super_arg.children.first).nil? && def_arg.name.nil?
111+
112+
def_arg.name == block_pass_child.children.first
113+
end
114+
115+
def forward_arg_same?(def_arg, super_arg)
116+
def_arg.forward_arg_type? && super_arg.forwarded_args_type?
117+
end
118+
119+
def define_method?(node)
120+
return false unless node.block_type?
121+
122+
node.method?(:define_method) || node.method?(:define_singleton_method)
123+
end
124+
125+
def preprocess_super_args(super_args)
126+
super_args.flat_map do |node|
127+
if node.hash_type? && !node.braces?
128+
node.children
129+
else
130+
node
131+
end
132+
end
133+
end
134+
end
135+
end
136+
end
137+
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', 'anonymous block forwarding', '&'
85+
end
86+
87+
context 'Ruby >= 3.2', :ruby32 do
88+
it_behaves_like 'offense', 'anonymous positional forwarding', '*'
89+
it_behaves_like 'offense', 'anonymous keyword forwarding', '**'
90+
91+
it_behaves_like 'no offense', 'mixed anonymous forwarding', '*, **', '*'
92+
it_behaves_like 'no offense', 'mixed anonymous 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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy