Class: RuboCop::Cop::Style::PreferItParameter

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector, TargetRubyVersion
Includes:
RangeHelp
Defined in:
lib/rubocop/cop/style/prefer_it_parameter.rb,
sig/rubocop/cop/style/prefer_it_parameter.rbs

Overview

Prefer the it block parameter over a named block argument in single-line blocks.

Only blocks consisting of a single statement are converted. This cop complements Style/ItBlockParameter in RuboCop core, which converts _1 to it but in its default style does not check named block arguments.

Examples:

# bad
users.map { |user| user.name.upcase }
items.select { |item| item.active? && item.visible? }

# good
users.map { it.name.upcase }
items.select { it.active? && it.visible? }

# good - multi-line block
users.each do |user|
  user.activate!
  notify(user)
end

# good - the block consists of two statements
items.each { |item| validate(item); save(item) }

# good - only the innermost block may use `it`
matrix.map { |row| row.map { it * 2 } }

# good - `|x,|` destructures the yielded value, unlike `it`
pairs.each { |pair,| puts pair }

# good - a callable's parameter list is part of its API
->(x) { puts x }
define_method(:m) { |x| x + 1 }

# bad
items.map { |item| {item:} }

# good - the value is spelled out, since `{it:}` would call a method named `it`
items.map { {item: it} }

Constant Summary collapse

MSG =

Returns:

  • (::String)
"Use the `it` block parameter instead of the named block argument `%<name>s`."
INNER_BLOCK_TYPES =

Signature:

  • Array[Symbol]

Returns:

  • (Array[Symbol])
%i[block numblock itblock].freeze
CALLABLE_METHODS =

Signature:

  • Array[Symbol]

Returns:

  • (Array[Symbol])
%i[define_method define_singleton_method lambda proc].freeze

Instance Method Summary collapse

Instance Method Details

#arguments_removal_range(node) ⇒ Parser::Source::Range

RBS:

  • node: RuboCop::AST::BlockNode

Parameters:

  • node (RuboCop::AST::BlockNode)

Returns:

  • (Parser::Source::Range)


227
228
229
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 227

def arguments_removal_range(node) #: Parser::Source::Range
  range_with_surrounding_space(node.arguments.source_range, side: :right, newlines: false)
end

#contains_block?(body) ⇒ Boolean

it is a syntax error inside a block that has an ordinary parameter, and silently shadows the outer one inside a parameterless block, so only the innermost block is converted.

RBS:

  • body: RuboCop::AST::Node

Parameters:

  • body (RuboCop::AST::Node)

Returns:

  • (Boolean)


165
166
167
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 165

def contains_block?(body) #: bool
  body.each_node(*INNER_BLOCK_TYPES).any?
end

#convertible_argument_name(node, body) ⇒ Symbol?

RBS:

  • node: RuboCop::AST::BlockNode

  • body: RuboCop::AST::Node

Parameters:

  • node (RuboCop::AST::BlockNode)
  • body (RuboCop::AST::Node)

Returns:

  • (Symbol, nil)


87
88
89
90
91
92
93
94
95
96
97
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 87

def convertible_argument_name(node, body) #: Symbol?
  return unless node.single_line?
  return if defines_callable?(node)

  name = sole_argument_name(node)
  return unless name
  return unless convertible_body?(body, name)
  return if shadows_it?(body)

  name
end

#convertible_body?(body, name) ⇒ Boolean

RBS:

  • body: RuboCop::AST::Node

  • name: Symbol

Parameters:

  • body (RuboCop::AST::Node)
  • name (Symbol)

Returns:

  • (Boolean)


145
146
147
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 145

def convertible_body?(body, name) #: bool
  single_statement?(body) && !contains_block?(body) && !reassigned?(body, name)
end

#defines_callable?(node) ⇒ Boolean

it drops the parameter name from Proc#parameters, which changes the meaning of a block that defines a callable object or a method: there the parameter list is part of the API, unlike a block passed to each or map.

RBS:

  • node: RuboCop::AST::BlockNode

Parameters:

  • node (RuboCop::AST::BlockNode)

Returns:

  • (Boolean)


113
114
115
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 113

def defines_callable?(node) #: bool
  CALLABLE_METHODS.include?(node.method_name) || proc_new?(node)
end

#lvar_references(body, name) ⇒ Array[RuboCop::AST::Node]

RBS:

  • body: RuboCop::AST::Node

  • name: Symbol

Parameters:

  • body (RuboCop::AST::Node)
  • name (Symbol)

Returns:

  • (Array[RuboCop::AST::Node])


196
197
198
199
200
201
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 196

def lvar_references(body, name) #: Array[RuboCop::AST::Node]
  body.each_node(:lvar).select do |node|
    variable = node #: RuboCop::AST::VarNode
    variable.name == name
  end
end

#omitted_value_pair(reference) ⇒ RuboCop::AST::PairNode?

Returns the enclosing pair when the reference is a value omission ({x:}, foo(x:)). Such a reference shares its source range with the label, so the value has to be written after the pair rather than replaced.

RBS:

  • reference: RuboCop::AST::Node

Parameters:

  • reference (RuboCop::AST::Node)

Returns:

  • (RuboCop::AST::PairNode, nil)


186
187
188
189
190
191
192
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 186

def omitted_value_pair(reference) #: RuboCop::AST::PairNode?
  parent = reference.parent
  return unless parent&.pair_type?

  pair = parent #: RuboCop::AST::PairNode
  pair if pair.value_omission?
end

#on_block(node) ⇒ void

This method returns an undefined value.

RBS:

  • node: RuboCop::AST::BlockNode

Parameters:

  • node (RuboCop::AST::BlockNode)


70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 70

def on_block(node) #: void
  body = node.body
  return unless body

  name = convertible_argument_name(node, body)
  return unless name

  references = lvar_references(body, name)
  return if references.empty?

  register_offense(node, name, references)
end

#proc_new?(node) ⇒ Boolean

RBS:

  • node: RuboCop::AST::BlockNode

Parameters:

  • node (RuboCop::AST::BlockNode)

Returns:

  • (Boolean)


118
119
120
121
122
123
124
125
126
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 118

def proc_new?(node) #: bool
  return false unless node.method?(:new)

  receiver = node.send_node.receiver
  return false unless receiver&.const_type?

  const = receiver #: RuboCop::AST::ConstNode
  const.short_name == :Proc
end

#reassigned?(body, name) ⇒ Boolean

A block that rebinds the name cannot be converted: the value it refers to would no longer be the one the block was yielded. match_var covers pattern matching (1 in x), which rebinds just like an assignment.

RBS:

  • body: RuboCop::AST::Node

  • name: Symbol

Parameters:

  • body (RuboCop::AST::Node)
  • name (Symbol)

Returns:

  • (Boolean)


175
176
177
178
179
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 175

def reassigned?(body, name) #: bool
  body.each_node(:lvasgn, :match_var).any? do |node|
    node.to_a.first == name
  end
end

#register_offense(node, name, references) ⇒ void

This method returns an undefined value.

RBS:

  • node: RuboCop::AST::BlockNode

  • name: Symbol

  • references: Array[RuboCop::AST::Node]

Parameters:

  • node (RuboCop::AST::BlockNode)
  • name (Symbol)
  • references (Array[RuboCop::AST::Node])


206
207
208
209
210
211
212
213
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 206

def register_offense(node, name, references) #: void
  add_offense(node.arguments, message: format(MSG, name:)) do |corrector|
    references.each do |reference|
      replace_reference(corrector, reference)
    end
    corrector.remove(arguments_removal_range(node))
  end
end

#replace_reference(corrector, reference) ⇒ void

This method returns an undefined value.

RBS:

  • corrector: RuboCop::Cop::Corrector

  • reference: RuboCop::AST::Node

Parameters:

  • corrector (RuboCop::Cop::Corrector)
  • reference (RuboCop::AST::Node)


217
218
219
220
221
222
223
224
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 217

def replace_reference(corrector, reference) #: void
  pair = omitted_value_pair(reference)
  if pair
    corrector.insert_after(pair.source_range, " it")
  else
    corrector.replace(reference.source_range, "it")
  end
end

#shadows_it?(body) ⇒ Boolean

it would not mean what the block expects when a local variable named it is already in scope, or when the body assigns to it — assigning turns it into a plain local variable and disables the implicit block parameter.

RBS:

  • body: RuboCop::AST::Node

Parameters:

  • body (RuboCop::AST::Node)

Returns:

  • (Boolean)


104
105
106
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 104

def shadows_it?(body) #: bool
  lvar_references(body, :it).any? || reassigned?(body, :it)
end

#single_statement?(body) ⇒ Boolean

begin (parentheses) and kwbegin (begin ... end) both wrap a sequence of statements as well as a single expression, so the number of children is what tells the two apart.

RBS:

  • body: RuboCop::AST::Node

Parameters:

  • body (RuboCop::AST::Node)

Returns:

  • (Boolean)


154
155
156
157
158
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 154

def single_statement?(body) #: bool
  return body.each_child_node.one? if body.type?(:begin, :kwbegin)

  true
end

#sole_argument_name(node) ⇒ Symbol?

RBS:

  • node: RuboCop::AST::BlockNode

Parameters:

  • node (RuboCop::AST::BlockNode)

Returns:

  • (Symbol, nil)


129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/rubocop/cop/style/prefer_it_parameter.rb', line 129

def sole_argument_name(node) #: Symbol?
  return unless node.argument_list.one?

  argument = node.first_argument
  # Rules out optarg, restarg, kwarg, blockarg, shadowarg and mlhs at once.
  return unless argument&.arg_type?
  # `|x,|` is indistinguishable from `|x|` in the AST even though it
  # destructures the yielded value, so the source has to be checked.
  return if node.arguments.source&.include?(",")

  plain_argument = argument #: RuboCop::AST::ArgNode
  plain_argument.name
end