Skip to content

Commit

Permalink
[Fix rubocop#11696] Add new Style/DataInheritance cop
Browse files Browse the repository at this point in the history
  • Loading branch information
Karol Topolski authored and ktopolski committed Apr 2, 2023
1 parent 6cb7343 commit bc42301
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/new_add_new_style_data_inheritance_cop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#11696](https://github.com/rubocop/rubocop/issues/11696): Add new `Style/DataInheritance` cop. ([@ktopolski][])
6 changes: 6 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3508,6 +3508,12 @@ Style/Copyright:
Notice: '^Copyright (\(c\) )?2[0-9]{3} .+'
AutocorrectNotice: ''

Style/DataInheritance:
Description: 'Checks for inheritance from Data.define.'
StyleGuide: '#no-extend-data-define'
Enabled: pending
VersionAdded: '<<next>>'

Style/DateTime:
Description: 'Use Time over DateTime.'
StyleGuide: '#date-time'
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@
require_relative 'rubocop/cop/style/conditional_assignment'
require_relative 'rubocop/cop/style/constant_visibility'
require_relative 'rubocop/cop/style/copyright'
require_relative 'rubocop/cop/style/data_inheritance'
require_relative 'rubocop/cop/style/date_time'
require_relative 'rubocop/cop/style/def_with_parentheses'
require_relative 'rubocop/cop/style/dir'
Expand Down
75 changes: 75 additions & 0 deletions lib/rubocop/cop/style/data_inheritance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Style
# Checks for inheritance from `Data.define` to avoid creating the anonymous parent class.
#
# @safety
# Autocorrection is unsafe because it will change the inheritance
# tree (e.g. return value of `Module#ancestors`) of the constant.
#
# @example
# # bad
# class Person < Data.define(:first_name, :last_name)
# def age
# 42
# end
# end
#
# # good
# Person = Data.define(:first_name, :last_name) do
# def age
# 42
# end
# end
class DataInheritance < Base
include RangeHelp
extend AutoCorrector
extend TargetRubyVersion

MSG = "Don't extend an instance initialized by `Data.define`. " \
'Use a block to customize the class.'

minimum_target_ruby_version 3.2

def on_class(node)
return unless data_define?(node.parent_class)

add_offense(node.parent_class.source_range) do |corrector|
corrector.remove(range_with_surrounding_space(node.loc.keyword, newlines: false))
corrector.replace(node.loc.operator, '=')

correct_parent(node.parent_class, corrector)
end
end

# @!method data_define?(node)
def_node_matcher :data_define?, <<~PATTERN
{(send (const {nil? cbase} :Data) :define ...)
(block (send (const {nil? cbase} :Data) :define ...) ...)}
PATTERN

private

def correct_parent(parent, corrector)
if parent.block_type?
corrector.remove(range_with_surrounding_space(parent.loc.end, newlines: false))
elsif (class_node = parent.parent).body.nil?
corrector.remove(range_for_empty_class_body(class_node, parent))
else
corrector.insert_after(parent, ' do')
end
end

def range_for_empty_class_body(class_node, data_define)
if class_node.single_line?
range_between(data_define.source_range.end_pos, class_node.source_range.end_pos)
else
range_by_whole_lines(class_node.loc.end, include_final_newline: true)
end
end
end
end
end
end
218 changes: 218 additions & 0 deletions spec/rubocop/cop/style/data_inheritance_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Style::DataInheritance, :config do
context 'Ruby >= 3.2', :ruby32 do
it 'registers an offense when extending instance of `Data.define`' do
expect_offense(<<~RUBY)
class Person < Data.define(:first_name, :last_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
def foo; end
end
RUBY

expect_correction(<<~RUBY)
Person = Data.define(:first_name, :last_name) do
def foo; end
end
RUBY
end

it 'registers an offense when extending instance of `::Data.define`' do
expect_offense(<<~RUBY)
class Person < ::Data.define(:first_name, :last_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
def foo; end
end
RUBY

expect_correction(<<~RUBY)
Person = ::Data.define(:first_name, :last_name) do
def foo; end
end
RUBY
end

it 'registers an offense when extending instance of `Data.define` with do ... end' do
expect_offense(<<~RUBY)
class Person < Data.define(:first_name, :last_name) do end
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
end
RUBY

expect_correction(<<~RUBY)
Person = Data.define(:first_name, :last_name) do
end
RUBY
end

it 'registers an offense when extending instance of `Data.define` without `do` ... `end` and class body is empty' do
expect_offense(<<~RUBY)
class Person < Data.define(:first_name, :last_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
end
RUBY

expect_correction(<<~RUBY)
Person = Data.define(:first_name, :last_name)
RUBY
end

it 'registers an offense when extending instance of `Data.define` without `do` ... `end` and class body is empty and single line definition' do
expect_offense(<<~RUBY)
class Person < Data.define(:first_name, :last_name); end
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
RUBY

expect_correction(<<~RUBY)
Person = Data.define(:first_name, :last_name)
RUBY
end

it 'registers an offense when extending instance of `::Data.define` with do ... end' do
expect_offense(<<~RUBY)
class Person < ::Data.define(:first_name, :last_name) do end
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
end
RUBY

expect_correction(<<~RUBY)
Person = ::Data.define(:first_name, :last_name) do
end
RUBY
end

it 'registers an offense when extending instance of `Data.define` when there is a comment ' \
'before class declaration' do
expect_offense(<<~RUBY)
# comment
class Person < Data.define(:first_name, :last_name) do end
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't extend an instance initialized by `Data.define`. Use a block to customize the class.
end
RUBY

expect_correction(<<~RUBY)
# comment
Person = Data.define(:first_name, :last_name) do
end
RUBY
end

it 'accepts plain class' do
expect_no_offenses(<<~RUBY)
class Person
end
RUBY
end

it 'accepts extending DelegateClass' do
expect_no_offenses(<<~RUBY)
class Person < DelegateClass(Animal)
end
RUBY
end

it 'accepts assignment to `Data.define`' do
expect_no_offenses('Person = Data.define(:first_name, :last_name)')
end

it 'accepts assignment to `::Data.define`' do
expect_no_offenses('Person = ::Data.define(:first_name, :last_name)')
end

it 'accepts assignment to block form of `Data.define`' do
expect_no_offenses(<<~RUBY)
Person = Data.define(:first_name, :last_name) do
def age
42
end
end
RUBY
end
end

context 'Ruby <= 3.1', :ruby31 do
it 'accepts extending instance of `Data.define`' do
expect_no_offenses(<<~RUBY)
class Person < Data.define(:first_name, :last_name)
def foo; end
end
RUBY
end

it 'accepts extending instance of `::Data.define`' do
expect_no_offenses(<<~RUBY)
class Person < ::Data.define(:first_name, :last_name)
def foo; end
end
RUBY
end

it 'accepts extending instance of `Data.define` with do ... end' do
expect_no_offenses(<<~RUBY)
class Person < Data.define(:first_name, :last_name) do end
end
RUBY
end

it 'accepts extending instance of `Data.define` without `do` ... `end` and class body is empty' do
expect_no_offenses(<<~RUBY)
class Person < Data.define(:first_name, :last_name)
end
RUBY
end

it 'accepts extending instance of `Data.define` without `do` ... `end` and class body is empty and single line definition' do
expect_no_offenses(<<~RUBY)
class Person < Data.define(:first_name, :last_name); end
RUBY
end

it 'accepts extending instance of `::Data.define` with do ... end' do
expect_no_offenses(<<~RUBY)
class Person < ::Data.define(:first_name, :last_name) do end
end
RUBY
end

it 'accepts extending instance of `Data.define` when there is a comment ' \
'before class declaration' do
expect_no_offenses(<<~RUBY)
# comment
class Person < Data.define(:first_name, :last_name) do end
end
RUBY
end

it 'accepts plain class' do
expect_no_offenses(<<~RUBY)
class Person
end
RUBY
end

it 'accepts extending DelegateClass' do
expect_no_offenses(<<~RUBY)
class Person < DelegateClass(Animal)
end
RUBY
end

it 'accepts assignment to `Data.define`' do
expect_no_offenses('Person = Data.define(:first_name, :last_name)')
end

it 'accepts assignment to `::Data.define`' do
expect_no_offenses('Person = ::Data.define(:first_name, :last_name)')
end

it 'accepts assignment to block form of `Data.define`' do
expect_no_offenses(<<~RUBY)
Person = Data.define(:first_name, :last_name) do
def age
42
end
end
RUBY
end
end
end

0 comments on commit bc42301

Please sign in to comment.