Skip to content

Commit

Permalink
Add new Lint/ToEnumArguments cop
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima authored and bbatsov committed Oct 27, 2020
1 parent d459e2d commit 21e4062
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/new_to_enum_arguments_cop.md
@@ -0,0 +1 @@
* [#7753](https://github.com/rubocop-hq/rubocop/issues/7753): Add new `Lint/ToEnumArguments` cop. ([@fatkodima][])
5 changes: 5 additions & 0 deletions config/default.yml
Expand Up @@ -1921,6 +1921,11 @@ Lint/Syntax:
VersionAdded: '0.9'


Lint/ToEnumArguments:
Description: 'This cop ensures that `to_enum`/`enum_for`, called for the current method, has correct arguments.'
Enabled: pending
VersionAdded: '1.1'

Lint/ToJSON:
Description: 'Ensure #to_json includes an optional argument.'
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Expand Up @@ -270,6 +270,7 @@ In the following section you find all available cops:
* xref:cops_lint.adoc#lintstructnewoverride[Lint/StructNewOverride]
* xref:cops_lint.adoc#lintsuppressedexception[Lint/SuppressedException]
* xref:cops_lint.adoc#lintsyntax[Lint/Syntax]
* xref:cops_lint.adoc#linttoenumarguments[Lint/ToEnumArguments]
* xref:cops_lint.adoc#linttojson[Lint/ToJSON]
* xref:cops_lint.adoc#linttoplevelreturnwithargument[Lint/TopLevelReturnWithArgument]
* xref:cops_lint.adoc#linttrailingcommainattributedeclaration[Lint/TrailingCommaInAttributeDeclaration]
Expand Down
40 changes: 40 additions & 0 deletions docs/modules/ROOT/pages/cops_lint.adoc
Expand Up @@ -4025,6 +4025,46 @@ end
This cop repacks Parser's diagnostics/errors
into RuboCop's offenses.

== Lint/ToEnumArguments

|===
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged

| Pending
| Yes
| No
| 1.1
| -
|===

This cop ensures that `to_enum`/`enum_for`, called for the current method,
has correct arguments.

=== Examples

[source,ruby]
----
# bad
def method(x, y = 1)
return to_enum(__method__, x) # `y` is missing
end
# good
def method(x, y = 1)
return to_enum(__method__, x, y)
end
# bad
def method(required:)
return to_enum(:method, required: something) # `required` has incorrect value
end
# good
def method(required:)
return to_enum(:method, required: required)
end
----

== Lint/ToJSON

|===
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -333,6 +333,7 @@
require_relative 'rubocop/cop/lint/struct_new_override'
require_relative 'rubocop/cop/lint/suppressed_exception'
require_relative 'rubocop/cop/lint/syntax'
require_relative 'rubocop/cop/lint/to_enum_arguments'
require_relative 'rubocop/cop/lint/to_json'
require_relative 'rubocop/cop/lint/top_level_return_with_argument'
require_relative 'rubocop/cop/lint/trailing_comma_in_attribute_declaration'
Expand Down
94 changes: 94 additions & 0 deletions lib/rubocop/cop/lint/to_enum_arguments.rb
@@ -0,0 +1,94 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Lint
# This cop ensures that `to_enum`/`enum_for`, called for the current method,
# has correct arguments.
#
# @example
# # bad
# def method(x, y = 1)
# return to_enum(__method__, x) # `y` is missing
# end
#
# # good
# def method(x, y = 1)
# return to_enum(__method__, x, y)
# end
#
# # bad
# def method(required:)
# return to_enum(:method, required: something) # `required` has incorrect value
# end
#
# # good
# def method(required:)
# return to_enum(:method, required: required)
# end
#
class ToEnumArguments < Base
MSG = 'Ensure you correctly provided all the arguments.'

RESTRICT_ON_SEND = %i[to_enum enum_for].freeze

def_node_matcher :enum_conversion_call?, <<~PATTERN
(send {nil? self} {:to_enum :enum_for} $_ $...)
PATTERN

def_node_matcher :method_name?, <<~PATTERN
{(send nil? :__method__) (sym %1)}
PATTERN

def_node_matcher :passing_keyword_arg?, <<~PATTERN
(pair (sym %1) (lvar %1))
PATTERN

# TODO: add support for argument forwarding (`...`) when ruby 3.0 is released
def on_send(node)
def_node = node.each_ancestor(:def, :defs).first
return unless def_node

enum_conversion_call?(node) do |method_node, arguments|
add_offense(node) unless method_name?(method_node, def_node.method_name) &&
arguments_match?(arguments, def_node)
end
end

private

def arguments_match?(arguments, def_node)
index = 0

def_node.arguments.reject(&:blockarg_type?).all? do |def_arg|
send_arg = arguments[index]
case def_arg.type
when :arg, :restarg, :optarg
index += 1
end

send_arg && argument_match?(send_arg, def_arg)
end
end

# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
def argument_match?(send_arg, def_arg)
def_arg_name = def_arg.children[0]

case def_arg.type
when :arg, :restarg
send_arg.source == def_arg.source
when :optarg
send_arg.source == def_arg_name.to_s
when :kwoptarg, :kwarg
send_arg.hash_type? &&
send_arg.pairs.any? { |pair| passing_keyword_arg?(pair, def_arg_name) }
when :kwrestarg
send_arg.each_child_node(:kwsplat).any? { |child| child.source == def_arg.source }
end
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
end
end
end
end
134 changes: 134 additions & 0 deletions spec/rubocop/cop/lint/to_enum_arguments_spec.rb
@@ -0,0 +1,134 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Lint::ToEnumArguments do
subject(:cop) { described_class.new }

it 'registers an offense when required arg is missing' do
expect_offense(<<~RUBY)
def m(x)
return to_enum(:m) unless block_given?
^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'registers an offense when optional arg is missing' do
expect_offense(<<~RUBY)
def m(x, y = 1)
return to_enum(:m, x) unless block_given?
^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'registers an offense when splat arg is missing' do
expect_offense(<<~RUBY)
def m(x, y = 1, *args)
return to_enum(:m, x, y) unless block_given?
^^^^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'registers an offense when required keyword arg is missing' do
expect_offense(<<~RUBY)
def m(x, y = 1, *args, required:)
return to_enum(:m, x, y, *args) unless block_given?
^^^^^^^^^^^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'registers an offense when optional keyword arg is missing' do
expect_offense(<<~RUBY)
def m(x, y = 1, *args, required:, optional: true)
return to_enum(:m, x, y, *args, required: required) unless block_given?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'registers an offense when splat keyword arg is missing' do
expect_offense(<<~RUBY)
def m(x, y = 1, *args, required:, optional: true, **kwargs)
return to_enum(:m, x, y, *args, required: required, optional: optional) unless block_given?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'registers an offense when arguments are swapped' do
expect_offense(<<~RUBY)
def m(x, y = 1)
return to_enum(:m, y, x) unless block_given?
^^^^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'registers an offense when other values are passed for keyword arguments' do
expect_offense(<<~RUBY)
def m(required:, optional: true)
return to_enum(:m, required: something_else, optional: optional) unless block_given?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'does not register an offense when not inside method definition' do
expect_no_offenses(<<~RUBY)
to_enum(:m)
RUBY
end

it 'does not register an offense when method call has a receiver other than `self`' do
expect_no_offenses(<<~RUBY)
def m(x)
return foo.to_enum(:m) unless block_given?
end
RUBY
end

it 'registers an offense when method is called on `self`' do
expect_offense(<<~RUBY)
def m(x)
return self.to_enum(:m) unless block_given?
^^^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'ignores the block argument' do
expect_no_offenses(<<~RUBY)
def m(x, &block)
return to_enum(:m, x) unless block_given?
end
RUBY
end

it 'registers an offense when enumerator is created for another method' do
expect_offense(<<~RUBY)
def m(x)
return to_enum(:not_m) unless block_given?
^^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'registers an offense when enumerator is created for `__method__`' do
expect_offense(<<~RUBY)
def m(x)
return to_enum(__method__) unless block_given?
^^^^^^^^^^^^^^^^^^^ Ensure you correctly provided all the arguments.
end
RUBY
end

it 'does not register an offense when enumerator is created with the correct arguments' do
expect_no_offenses(<<~RUBY)
def m(x, y = 1, *args, required:, optional: true, **kwargs, &block)
return to_enum(:m, x, y, *args, required: required, optional: optional, **kwargs) unless block_given?
end
RUBY
end
end

0 comments on commit 21e4062

Please sign in to comment.