Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix #8663] Fix issues with Style/ClassMethodsDefinitions #8687

Merged
merged 2 commits into from Sep 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -44,6 +44,7 @@
* [#8617](https://github.com/rubocop-hq/rubocop/issues/8617): Fix `Style/HashAsLastArrayItem` to not register an offense when all items in an array are hashes. ([@dvandersluis][])
* [#8500](https://github.com/rubocop-hq/rubocop/issues/8500): Add `in?` to AllowedMethods for `Lint/SafeNavigationChain` cop. ([@tejasbubane][])
* [#8629](https://github.com/rubocop-hq/rubocop/pull/8629): Fix the cache being reusable in CI by using crc32 to calculate file hashes rather than `mtime`, which changes each CI build. ([@dvandersluis][])
* [#8663](https://github.com/rubocop-hq/rubocop/issues/8663): Fix multiple autocorrection bugs with `Style/ClassMethodsDefinitions`. ([@dvandersluis][])
* [#8621](https://github.com/rubocop-hq/rubocop/issues/8621): Add helpful Infinite Loop error message. ([@iSarCasm][])

## 0.90.0 (2020-09-01)
Expand Down
12 changes: 3 additions & 9 deletions lib/rubocop/cop/mixin/comments_help.rb
Expand Up @@ -7,15 +7,9 @@ module CommentsHelp
include VisibilityHelp

def source_range_with_comment(node)
begin_pos, end_pos =
if node.def_type?
start_node = find_visibility_start(node) || node
end_node = find_visibility_end(node) || node
[begin_pos_with_comment(start_node),
end_position_for(end_node) + 1]
else
[begin_pos_with_comment(node), end_position_for(node)]
end
begin_pos = begin_pos_with_comment(node)
end_pos = end_position_for(node)
end_pos += 1 if node.def_type?
Comment on lines -10 to +12
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned, this method was incorrectly getting a range for not just the def, but all defs within the same visibility group. I've fixed it to just return the def and its preceding comments (this mixin is only in use for this cop, so this should not affect anything else).


Parser::Source::Range.new(buffer, begin_pos, end_pos)
end
Expand Down
58 changes: 42 additions & 16 deletions lib/rubocop/cop/style/class_methods_definitions.rb
Expand Up @@ -62,22 +62,19 @@ class ClassMethodsDefinitions < Base
include ConfigurableEnforcedStyle
include CommentsHelp
include VisibilityHelp
include RangeHelp
extend AutoCorrector

MSG = 'Use `%<preferred>s` to define class method.'
MSG = 'Use `%<preferred>s` to define a class method.'
MSG_SCLASS = 'Do not define public methods within class << self.'

def on_sclass(node)
return unless def_self_style?
return unless node.identifier.source == 'self'
return if contains_non_public_methods?(node)
return unless all_methods_public?(node)

def_nodes(node).each do |def_node|
next unless node_visibility(def_node) == :public

message = format(MSG, preferred: "def self.#{def_node.method_name}")
add_offense(def_node, message: message) do |corrector|
extract_def_from_sclass(def_node, node, corrector)
end
add_offense(node, message: MSG_SCLASS) do |corrector|
autocorrect_sclass(node, corrector)
end
end

Expand All @@ -94,8 +91,11 @@ def def_self_style?
style == :def_self
end

def contains_non_public_methods?(sclass_node)
def_nodes(sclass_node).any? { |def_node| node_visibility(def_node) != :public }
def all_methods_public?(sclass_node)
def_nodes = def_nodes(sclass_node)
return false if def_nodes.empty?

def_nodes.all? { |def_node| node_visibility(def_node) == :public }
end

def def_nodes(sclass_node)
Expand All @@ -111,19 +111,45 @@ def def_nodes(sclass_node)
end
end

def extract_def_from_sclass(def_node, sclass_node, corrector)
def autocorrect_sclass(node, corrector)
rewritten_defs = []

def_nodes(node).each do |def_node|
next unless node_visibility(def_node) == :public

range, source = extract_def_from_sclass(def_node, node)

corrector.remove(range)
rewritten_defs << source
end

if sclass_only_has_methods?(node)
corrector.remove(node)
rewritten_defs.first&.strip!
else
corrector.insert_after(node, "\n")
end

corrector.insert_after(node, rewritten_defs.join("\n"))
end

def sclass_only_has_methods?(node)
node.body.def_type? || node.body.each_child_node.all?(&:def_type?)
end

def extract_def_from_sclass(def_node, sclass_node)
range = source_range_with_comment(def_node)
source = range.source.sub!(
"def #{def_node.method_name}",
"def self.#{def_node.method_name}"
)

corrector.insert_before(sclass_node, "#{source}\n#{indent(sclass_node)}")
corrector.remove(range)
source = source.gsub(/^ {#{indentation_diff(def_node, sclass_node)}}/, '')
[range, source.chomp]
end

def indent(node)
' ' * node.loc.column
def indentation_diff(node1, node2)
node1.loc.column - node2.loc.column
end
end
end
Expand Down
132 changes: 119 additions & 13 deletions spec/rubocop/cop/style/class_methods_definitions_spec.rb
Expand Up @@ -10,24 +10,23 @@
expect_offense(<<~RUBY)
class A
class << self
^^^^^^^^^^^^^ Do not define public methods within class << self.
attr_reader :two

def three
^^^^^^^^^ Use `def self.three` to define class method.
end
end
end
RUBY

expect_correction(<<~RUBY)
class A
#{trailing_whitespace}
def self.three
end

class << self
attr_reader :two
end

def self.three
end
end
RUBY
end
Expand All @@ -36,27 +35,113 @@ class << self
expect_offense(<<~RUBY)
class A
class << self
^^^^^^^^^^^^^ Do not define public methods within class << self.
attr_reader :one

# Multiline
# comment.
def two
^^^^^^^ Use `def self.two` to define class method.
end
end
end
RUBY

expect_correction(<<~RUBY)
class A
#{trailing_whitespace}
# Multiline
# comment.
def self.two
class << self
attr_reader :one
end

# Multiline
# comment.
def self.two
end
end
RUBY
end

it 'correctly handles class << self containing multiple methods' do
expect_offense(<<~RUBY)
class A
class << self
^^^^^^^^^^^^^ Do not define public methods within class << self.
def one
:one
end

def two
:two
end
end
end
RUBY

expect_correction(<<~RUBY)
class A
def self.one
:one
end

def self.two
:two
end
end
RUBY
end

it 'removes empty class << self when correcting' do
expect_offense(<<~RUBY)
class A
def self.one
end

class << self
attr_reader :one
^^^^^^^^^^^^^ Do not define public methods within class << self.
def two
end

def three
end
end
end
RUBY

expect_correction(<<~RUBY)
class A
def self.one
end

def self.two
end

def self.three
end
end
RUBY
end

it 'correctly handles def self.x within class << self' do
expect_offense(<<~RUBY)
class A
class << self
^^^^^^^^^^^^^ Do not define public methods within class << self.
def self.one
end

def two
end
end
end
RUBY

expect_correction(<<~RUBY)
class A
class << self
def self.one
end
end

def self.two
end
end
RUBY
Expand All @@ -71,13 +156,23 @@ def one

private

def one
def two
end
end
end
RUBY
end

it 'does not register an offense when class << self does not contain methods' do
expect_no_offenses(<<~RUBY)
class A
class << self
attr_reader :one
end
end
RUBY
end

it 'does not register an offense when defining class methods with `def self.method`' do
expect_no_offenses(<<~RUBY)
class A
Expand All @@ -97,6 +192,17 @@ def one
end
RUBY
end

it 'does not register an offense when class << self contains only class methods' do
expect_no_offenses(<<~RUBY)
class A
class << self
def self.one
end
end
end
RUBY
end
end

context 'when EnforcedStyle is self_class' do
Expand All @@ -108,7 +214,7 @@ def one
expect_offense(<<~RUBY)
class A
def self.one
^^^^^^^^^^^^ Use `class << self` to define class method.
^^^^^^^^^^^^ Use `class << self` to define a class method.
end
end
RUBY
Expand Down