rubocop-prefer_it_parameter 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5cc86f9725bb1e055df24642820dd33b8f6783b9a8b493f9ee4b8fbb74672107
4
- data.tar.gz: d6baada0d759d55b85a28966463ebb479d4db86aeaebe4cd09ea5bfeca4b9310
3
+ metadata.gz: a654f707a32decb9fe04e213f80d731587b30ca23daa159fabcf6b34d7ce1b9c
4
+ data.tar.gz: d3e5ba34a8f33a1d1064d1f2f7c7d0d276b90e94b01fff3ef35bcd433f695c99
5
5
  SHA512:
6
- metadata.gz: d5828d52ff9ecf241a7c27f8b623ce4b7c479492c96c0600c992bb39eb5620a9b65c2a6914d1bf4aeaae0451295a4961a1105bc5af8c1f05f2a731b51e30970b
7
- data.tar.gz: afd728b9c7367f1e4af45a695169fa3fd07fb1b52ca6ab855c56af99e09bacc312f1074458ba580befa36a8b769e5a170badf166ba0763ddd41538974825cf40
6
+ metadata.gz: b35242ffe25304f9790f90d9ad875529d07c0522ef9bf8d82ea3ff76d815eeb2cd864ab0e06a8bd0a8a821d255deb60b9df74e776b7b2342ba4b0be654306efa
7
+ data.tar.gz: 70af29593bdb22415d82812c08323d353e8c14d1447ea4c3d2c31f5715288e3dfd1c1eee72be7caccd8679fd7a3f2bcfe55cb144b48f70ad0dff10d41c71ab96
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2026-09-07
4
+
5
+ - `Style/PreferItParameter`: recognize RSpec's custom matcher DSL (`RSpec::Matchers.define`)
6
+ as a built-in exception, alongside `lambda`/`proc`/`Proc.new`/`define_method`/
7
+ `define_singleton_method`; these are always checked and cannot be disabled.
8
+ - `Style/PreferItParameter`: add the `IgnoredBlockContexts` config option, so a project
9
+ can name its own such unsafe blocks on top of the built-ins above. As part of this,
10
+ the built-in `Proc.new` check now matches by exact receiver source text rather than
11
+ resolved constant name, so a `Proc` reached through another namespace is no longer
12
+ recognized.
13
+
3
14
  ## [1.0.0] - 2026-08-05
4
15
 
5
16
  Initial release.
data/README.md CHANGED
@@ -70,6 +70,47 @@ A block is left alone when it:
70
70
  - references a local variable named `it` from an enclosing scope, or assigns to `it`
71
71
  - already names its argument `it` — dropping it would revive an `it` from an enclosing scope (`Style/ItAssignment` forbids the name instead)
72
72
  - defines a callable or a method — `->(x) { }`, `lambda`, `proc`, `Proc.new`, `define_method`, `define_singleton_method` — since the parameter list is part of its API and `it` drops the parameter name
73
+ - is nested inside RSpec's custom matcher DSL (`RSpec::Matchers.define`) as `match`, `match_when_negated`, `match_unless_raises`, `chain`, `failure_message`, `failure_message_when_negated` or `description` — these blocks are turned into actual methods internally, where `it`'s implicit binding does not reliably survive (see below)
74
+ - matches a project-specific entry in `IgnoredBlockContexts` (see below)
75
+
76
+ The `Proc.new`/`lambda`/etc. and RSpec cases above are built into the cop and always checked — they cannot be turned off. `IgnoredBlockContexts` is how a project adds its own such cases on top of them.
77
+
78
+ #### `IgnoredBlockContexts`
79
+
80
+ A block can be unsafe to convert for reasons specific to how it's used, not just because of what method it's passed to — this is exactly why RSpec's matcher DSL and `Proc.new` are already built-in exceptions above. `IgnoredBlockContexts` lets a project name its *own* such cases the same way, each scoped to the call it's nested inside (or, for `Proc.new`-like cases, matched wherever it's called), so a generic method name doesn't suppress the check on unrelated code.
81
+
82
+ ```ruby
83
+ # bad
84
+ RSpec::Matchers.define :be_valid_foo do
85
+ match { it.is_a?(Foo) && it.valid? }
86
+ end
87
+
88
+ # good
89
+ RSpec::Matchers.define :be_valid_foo do
90
+ match { |actual| actual.is_a?(Foo) && actual.valid? }
91
+ end
92
+ ```
93
+
94
+ `IgnoredBlockContexts` is empty by default. The built-in cases above are not stored in this config option at all — they're checked independently — so setting `IgnoredBlockContexts` in your `.rubocop.yml` only *adds* entries; it can never disable a built-in case:
95
+
96
+ ```yaml
97
+ Style/PreferItParameter:
98
+ IgnoredBlockContexts:
99
+ OurDsl.define_matcher:
100
+ - our_custom_dsl_method
101
+ ```
102
+
103
+ A block named this way is only ignored when it's nested inside the matching enclosing call — so `our_custom_dsl_method { |x| x.foo }` written anywhere else is unaffected and still converted, even though the method name is the same. This is what keeps a generic name from silently suppressing the check on unrelated code.
104
+
105
+ An enclosing call written without a receiver (`define_matcher:` instead of `OurDsl.define_matcher:`) only matches a bare call with no receiver at all. Write `*.define_matcher:` to match regardless of receiver instead — the same way `lambda` or `define_method` are matched in the exceptions list above. A value of `nil` (an entry with nothing under it) means the call itself is unsafe wherever it appears, with no nesting required — this is how the built-in `Proc.new` case works internally:
106
+
107
+ ```yaml
108
+ Style/PreferItParameter:
109
+ IgnoredBlockContexts:
110
+ "*.our_dsl_method": # any receiver, no nesting required
111
+ OurDsl.define_matcher: # this exact receiver, blocks nested inside it
112
+ - our_custom_dsl_method
113
+ ```
73
114
 
74
115
  ## Related cops
75
116
 
@@ -105,6 +146,7 @@ The autocorrection is marked unsafe (`SafeAutoCorrect: false`), so `rubocop -a`
105
146
  - A local variable or parameter named `it` takes precedence over the block parameter. The cop skips a block that references such a variable, but it cannot detect one that the block never references — there the autocorrection silently changes what the block sees. Enabling `Style/ItAssignment` is therefore a prerequisite.
106
147
  - `Proc#parameters` loses the argument name: `[[:opt, :x]]` becomes `[[:opt]]`. Blocks that define a callable or a method are excluded for this reason, but a block captured with `&block` and introspected elsewhere is still affected.
107
148
  - `binding.local_variable_get(:x)` inside the block stops working.
149
+ - A block can be unsafe to convert for reasons specific to its own context — e.g. a library that turns it into an actual method internally may stop passing it an argument at all. RSpec's custom matcher DSL is built in for this reason (see [Exceptions](#exceptions)); [`IgnoredBlockContexts`](#ignoredblockcontexts) lets a project name the same kind of case for a library this cop doesn't know about.
108
150
 
109
151
  ## Requirements
110
152
 
data/config/default.yml CHANGED
@@ -4,3 +4,4 @@ Style/PreferItParameter:
4
4
  Enabled: true
5
5
  SafeAutoCorrect: false
6
6
  VersionAdded: '1.0'
7
+ IgnoredBlockContexts: {}
@@ -15,9 +15,19 @@ module RuboCop
15
15
  # change what the block sees. Enabling `Style/ItAssignment` rules such names
16
16
  # out. Additionally, `it` drops the argument name from `Proc#parameters`
17
17
  # (`[[:opt, :x]]` becomes `[[:opt]]`) and breaks
18
- # `binding.local_variable_get(:x)` inside the block. Blocks that define a
19
- # callable or a method are skipped for that reason, but a block captured with
20
- # `&block` and introspected elsewhere is still affected.
18
+ # `binding.local_variable_get(:x)` inside the block. A block captured with
19
+ # `&block` and introspected elsewhere is still affected even when none of
20
+ # the below applies.
21
+ #
22
+ # Some blocks are unsafe to convert for reasons this cop cannot see from the
23
+ # AST alone — `->(x) { }`, `lambda`, `proc`, `Proc.new`, `define_method` and
24
+ # `define_singleton_method` all define a callable or a method, where the
25
+ # parameter list is part of its API and `it` drops the parameter name; RSpec's
26
+ # custom matcher DSL turns `match`, `chain` and similar blocks into actual
27
+ # methods internally, where `it`'s implicit binding does not reliably survive.
28
+ # These are all built in and cannot be turned off. `IgnoredBlockContexts` lets
29
+ # a project add its own cases the same way, scoped to where they're nested so
30
+ # a generic method name doesn't suppress the check on unrelated code.
21
31
  #
22
32
  # @example
23
33
  # # bad
@@ -47,6 +57,11 @@ module RuboCop
47
57
  # ->(x) { puts x }
48
58
  # define_method(:m) { |x| x + 1 }
49
59
  #
60
+ # # good - RSpec turns this block into a method internally, where `it` cannot be trusted
61
+ # RSpec::Matchers.define :be_valid_foo do
62
+ # match { |actual| actual.valid? }
63
+ # end
64
+ #
50
65
  # # bad
51
66
  # items.map { |item| {item:} }
52
67
  #
@@ -64,7 +79,25 @@ module RuboCop
64
79
 
65
80
  INNER_BLOCK_TYPES = %i[block numblock itblock].freeze #: Array[Symbol]
66
81
 
67
- CALLABLE_METHODS = %i[define_method define_singleton_method lambda proc].freeze #: Array[Symbol]
82
+ # Maps a call to the method names that are unsafe when nested inside it, in
83
+ # the same format as the `IgnoredBlockContexts` config option (see
84
+ # `matches_context?` for the exact matching rules). A `nil` value means the
85
+ # call itself is unsafe, wherever it appears, with no nesting required —
86
+ # `lambda`/`proc`/`define_method`/`define_singleton_method` match regardless
87
+ # of receiver (`*.`), `Proc.new` requires that exact receiver. These are core
88
+ # Ruby behavior and RSpec's widely-used matcher DSL, so unlike
89
+ # `IgnoredBlockContexts` they're built in and cannot be turned off.
90
+ BUILTIN_IGNORED_BLOCK_CONTEXTS = {
91
+ "*.lambda" => nil,
92
+ "*.proc" => nil,
93
+ "*.define_method" => nil,
94
+ "*.define_singleton_method" => nil,
95
+ "Proc.new" => nil,
96
+ "RSpec::Matchers.define" => %i[
97
+ match match_when_negated match_unless_raises chain
98
+ failure_message failure_message_when_negated description
99
+ ]
100
+ }.freeze #: Hash[String, Array[Symbol]?]
68
101
 
69
102
  # @rbs node: RuboCop::AST::BlockNode
70
103
  def on_block(node) #: void
@@ -86,7 +119,7 @@ module RuboCop
86
119
  # @rbs body: RuboCop::AST::Node
87
120
  def convertible_argument_name(node, body) #: Symbol?
88
121
  return unless node.single_line?
89
- return if defines_callable?(node)
122
+ return if ignored?(node)
90
123
 
91
124
  name = sole_argument_name(node)
92
125
  return unless name
@@ -105,24 +138,90 @@ module RuboCop
105
138
  lvar_references(body, :it).any? || reassigned?(body, :it)
106
139
  end
107
140
 
108
- # `it` drops the parameter name from `Proc#parameters`, which changes the
109
- # meaning of a block that defines a callable object or a method: there the
110
- # parameter list is part of the API, unlike a block passed to `each` or `map`.
141
+ # A block can be unsafe to convert for reasons specific to how it's used, not
142
+ # just because of what method it's passed to — see `BUILTIN_IGNORED_BLOCK_CONTEXTS`
143
+ # for the built-in cases and `matches_context?` for what a context means.
144
+ # `IgnoredBlockContexts` lets a project add its own cases the same way; it is
145
+ # checked separately from, and in addition to, the built-in ones, so a project
146
+ # can never accidentally disable those by overriding this config option.
111
147
  #
112
148
  # @rbs node: RuboCop::AST::BlockNode
113
- def defines_callable?(node) #: bool
114
- CALLABLE_METHODS.include?(node.method_name) || proc_new?(node)
149
+ def ignored?(node) #: bool
150
+ ignored_in?(node, BUILTIN_IGNORED_BLOCK_CONTEXTS) || ignored_in?(node, ignored_block_contexts)
115
151
  end
116
152
 
117
153
  # @rbs node: RuboCop::AST::BlockNode
118
- def proc_new?(node) #: bool
119
- return false unless node.method?(:new)
154
+ # @rbs contexts: Hash[String, Array[Symbol]?]
155
+ def ignored_in?(node, contexts) #: bool
156
+ contexts.any? { |context, method_names| matches_context?(node, context, method_names) }
157
+ end
120
158
 
121
- receiver = node.send_node.receiver
122
- return false unless receiver&.const_type?
159
+ # @rbs @ignored_block_contexts: Hash[String, Array[Symbol]?]
123
160
 
124
- const = receiver #: RuboCop::AST::ConstNode
125
- const.short_name == :Proc
161
+ def ignored_block_contexts #: Hash[String, Array[Symbol]?]
162
+ default = {} #: Hash[String, Array[String]?]
163
+ @ignored_block_contexts ||= cop_config.fetch("IgnoredBlockContexts", default).transform_values do |names|
164
+ names&.map(&:to_sym)
165
+ end
166
+ end
167
+
168
+ # A `nil` (or empty) `method_names` means `context` describes the node's own
169
+ # call, checked with no nesting required — this is how `Proc.new` and the
170
+ # receiver-blind `*.lambda`-style entries work. Otherwise, `context` names an
171
+ # enclosing call the node must be nested inside, and `method_names` are the
172
+ # names that are unsafe within it — this is how RSpec's matcher DSL works.
173
+ #
174
+ # @rbs node: RuboCop::AST::BlockNode
175
+ # @rbs context: String
176
+ # @rbs method_names: Array[Symbol]?
177
+ def matches_context?(node, context, method_names) #: bool
178
+ if method_names.nil? || method_names.empty?
179
+ receiver_source, method_name = parse_context(context)
180
+ context_call?(node.send_node, receiver_source, method_name)
181
+ else
182
+ method_names.include?(node.method_name) && nested_in_context?(node, context)
183
+ end
184
+ end
185
+
186
+ # `numblock` and `itblock` (a `_1` or `it` block) are included since
187
+ # rubocop-ast maps both to `BlockNode`, so `#send_node` works the same as for
188
+ # a plain `block`.
189
+ #
190
+ # @rbs node: RuboCop::AST::BlockNode
191
+ # @rbs context: String
192
+ def nested_in_context?(node, context) #: bool
193
+ receiver_source, method_name = parse_context(context)
194
+ node.each_ancestor(*INNER_BLOCK_TYPES).any? do |ancestor|
195
+ block_ancestor = ancestor #: RuboCop::AST::BlockNode
196
+ context_call?(block_ancestor.send_node, receiver_source, method_name)
197
+ end
198
+ end
199
+
200
+ # @rbs context: String
201
+ def parse_context(context) #: [String, String]
202
+ receiver_source, _dot, method_name = context.rpartition(".")
203
+ [receiver_source, method_name]
204
+ end
205
+
206
+ # `receiver_source` of `"*"` matches any receiver, or none. An empty
207
+ # `receiver_source` (`context` had no `.`) requires no receiver at all.
208
+ # Otherwise the receiver's source must match exactly, modulo a leading `::`
209
+ # on either side — `::Proc` is not receiver-less, its source is `"::Proc"`
210
+ # (a `cbase`-prefixed const), so this is what lets `Proc.new` also match
211
+ # `::Proc.new`.
212
+ #
213
+ # @rbs send_node: RuboCop::AST::SendNode
214
+ # @rbs receiver_source: String
215
+ # @rbs method_name: String
216
+ def context_call?(send_node, receiver_source, method_name) #: bool
217
+ return false unless send_node.method?(method_name.to_sym)
218
+ return true if receiver_source == "*"
219
+ return send_node.receiver.nil? if receiver_source.empty?
220
+
221
+ actual_receiver_source = send_node.receiver&.source
222
+ return false unless actual_receiver_source
223
+
224
+ actual_receiver_source.delete_prefix("::") == receiver_source.delete_prefix("::")
126
225
  end
127
226
 
128
227
  # @rbs node: RuboCop::AST::BlockNode
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module PreferItParameter
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
@@ -15,9 +15,19 @@ module RuboCop
15
15
  # change what the block sees. Enabling `Style/ItAssignment` rules such names
16
16
  # out. Additionally, `it` drops the argument name from `Proc#parameters`
17
17
  # (`[[:opt, :x]]` becomes `[[:opt]]`) and breaks
18
- # `binding.local_variable_get(:x)` inside the block. Blocks that define a
19
- # callable or a method are skipped for that reason, but a block captured with
20
- # `&block` and introspected elsewhere is still affected.
18
+ # `binding.local_variable_get(:x)` inside the block. A block captured with
19
+ # `&block` and introspected elsewhere is still affected even when none of
20
+ # the below applies.
21
+ #
22
+ # Some blocks are unsafe to convert for reasons this cop cannot see from the
23
+ # AST alone — `->(x) { }`, `lambda`, `proc`, `Proc.new`, `define_method` and
24
+ # `define_singleton_method` all define a callable or a method, where the
25
+ # parameter list is part of its API and `it` drops the parameter name; RSpec's
26
+ # custom matcher DSL turns `match`, `chain` and similar blocks into actual
27
+ # methods internally, where `it`'s implicit binding does not reliably survive.
28
+ # These are all built in and cannot be turned off. `IgnoredBlockContexts` lets
29
+ # a project add its own cases the same way, scoped to where they're nested so
30
+ # a generic method name doesn't suppress the check on unrelated code.
21
31
  #
22
32
  # @example
23
33
  # # bad
@@ -47,6 +57,11 @@ module RuboCop
47
57
  # ->(x) { puts x }
48
58
  # define_method(:m) { |x| x + 1 }
49
59
  #
60
+ # # good - RSpec turns this block into a method internally, where `it` cannot be trusted
61
+ # RSpec::Matchers.define :be_valid_foo do
62
+ # match { |actual| actual.valid? }
63
+ # end
64
+ #
50
65
  # # bad
51
66
  # items.map { |item| {item:} }
52
67
  #
@@ -63,7 +78,15 @@ module RuboCop
63
78
 
64
79
  INNER_BLOCK_TYPES: Array[Symbol]
65
80
 
66
- CALLABLE_METHODS: Array[Symbol]
81
+ # Maps a call to the method names that are unsafe when nested inside it, in
82
+ # the same format as the `IgnoredBlockContexts` config option (see
83
+ # `matches_context?` for the exact matching rules). A `nil` value means the
84
+ # call itself is unsafe, wherever it appears, with no nesting required —
85
+ # `lambda`/`proc`/`define_method`/`define_singleton_method` match regardless
86
+ # of receiver (`*.`), `Proc.new` requires that exact receiver. These are core
87
+ # Ruby behavior and RSpec's widely-used matcher DSL, so unlike
88
+ # `IgnoredBlockContexts` they're built in and cannot be turned off.
89
+ BUILTIN_IGNORED_BLOCK_CONTEXTS: Hash[String, Array[Symbol]?]
67
90
 
68
91
  # @rbs node: RuboCop::AST::BlockNode
69
92
  def on_block: (RuboCop::AST::BlockNode node) -> void
@@ -81,15 +104,57 @@ module RuboCop
81
104
  # @rbs body: RuboCop::AST::Node
82
105
  def shadows_it?: (RuboCop::AST::Node body) -> bool
83
106
 
84
- # `it` drops the parameter name from `Proc#parameters`, which changes the
85
- # meaning of a block that defines a callable object or a method: there the
86
- # parameter list is part of the API, unlike a block passed to `each` or `map`.
107
+ # A block can be unsafe to convert for reasons specific to how it's used, not
108
+ # just because of what method it's passed to — see `BUILTIN_IGNORED_BLOCK_CONTEXTS`
109
+ # for the built-in cases and `matches_context?` for what a context means.
110
+ # `IgnoredBlockContexts` lets a project add its own cases the same way; it is
111
+ # checked separately from, and in addition to, the built-in ones, so a project
112
+ # can never accidentally disable those by overriding this config option.
113
+ #
114
+ # @rbs node: RuboCop::AST::BlockNode
115
+ def ignored?: (RuboCop::AST::BlockNode node) -> bool
116
+
117
+ # @rbs node: RuboCop::AST::BlockNode
118
+ # @rbs contexts: Hash[String, Array[Symbol]?]
119
+ def ignored_in?: (RuboCop::AST::BlockNode node, Hash[String, Array[Symbol]?] contexts) -> bool
120
+
121
+ @ignored_block_contexts: Hash[String, Array[Symbol]?]
122
+
123
+ def ignored_block_contexts: () -> Hash[String, Array[Symbol]?]
124
+
125
+ # A `nil` (or empty) `method_names` means `context` describes the node's own
126
+ # call, checked with no nesting required — this is how `Proc.new` and the
127
+ # receiver-blind `*.lambda`-style entries work. Otherwise, `context` names an
128
+ # enclosing call the node must be nested inside, and `method_names` are the
129
+ # names that are unsafe within it — this is how RSpec's matcher DSL works.
87
130
  #
88
131
  # @rbs node: RuboCop::AST::BlockNode
89
- def defines_callable?: (RuboCop::AST::BlockNode node) -> bool
132
+ # @rbs context: String
133
+ # @rbs method_names: Array[Symbol]?
134
+ def matches_context?: (RuboCop::AST::BlockNode node, String context, Array[Symbol]? method_names) -> bool
90
135
 
136
+ # `numblock` and `itblock` (a `_1` or `it` block) are included since
137
+ # rubocop-ast maps both to `BlockNode`, so `#send_node` works the same as for
138
+ # a plain `block`.
139
+ #
91
140
  # @rbs node: RuboCop::AST::BlockNode
92
- def proc_new?: (RuboCop::AST::BlockNode node) -> bool
141
+ # @rbs context: String
142
+ def nested_in_context?: (RuboCop::AST::BlockNode node, String context) -> bool
143
+
144
+ # @rbs context: String
145
+ def parse_context: (String context) -> [ String, String ]
146
+
147
+ # `receiver_source` of `"*"` matches any receiver, or none. An empty
148
+ # `receiver_source` (`context` had no `.`) requires no receiver at all.
149
+ # Otherwise the receiver's source must match exactly, modulo a leading `::`
150
+ # on either side — `::Proc` is not receiver-less, its source is `"::Proc"`
151
+ # (a `cbase`-prefixed const), so this is what lets `Proc.new` also match
152
+ # `::Proc.new`.
153
+ #
154
+ # @rbs send_node: RuboCop::AST::SendNode
155
+ # @rbs receiver_source: String
156
+ # @rbs method_name: String
157
+ def context_call?: (RuboCop::AST::SendNode send_node, String receiver_source, String method_name) -> bool
93
158
 
94
159
  # @rbs node: RuboCop::AST::BlockNode
95
160
  def sole_argument_name: (RuboCop::AST::BlockNode node) -> Symbol?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-prefer_it_parameter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takeshi KOMIYA