Skip to content

Commit

Permalink
Add new Rails/ActiveRecordCallbacksOrder cop
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Jun 30, 2020
1 parent 840f152 commit 1328643
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### New features

* [#285](https://github.com/rubocop-hq/rubocop-rails/pull/285): Add new `Rails/ActiveRecordCallbacksOrder` cop. ([@fatkodima][])
* [#275](https://github.com/rubocop-hq/rubocop-rails/pull/275): Add new `Rails/MatchRoute` cop. ([@fatkodima][])
* [#271](https://github.com/rubocop-hq/rubocop-rails/pull/271): Add new `Rails/RenderInline` cop. ([@fatkodima][])
* [#281](https://github.com/rubocop-hq/rubocop-rails/pull/281): Add new `Rails/MailerName` cop. ([@fatkodima][])
Expand Down
6 changes: 6 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Rails/ActiveRecordAliases:
VersionAdded: '0.53'
SafeAutoCorrect: false

Rails/ActiveRecordCallbacksOrder:
Description: 'Order callback declarations in the order, in which they will be executed.'
StyleGuide: 'https://rails.rubystyle.guide/#callbacks-order'
Enabled: 'pending'
VersionAdded: '2.7'

Rails/ActiveRecordOverride:
Description: >-
Check for overriding Active Record methods instead of using
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* xref:cops_rails.adoc#railsactionfilter[Rails/ActionFilter]
* xref:cops_rails.adoc#railsactiverecordaliases[Rails/ActiveRecordAliases]
* xref:cops_rails.adoc#railsactiverecordcallbacksorder[Rails/ActiveRecordCallbacksOrder]
* xref:cops_rails.adoc#railsactiverecordoverride[Rails/ActiveRecordOverride]
* xref:cops_rails.adoc#railsactivesupportaliases[Rails/ActiveSupportAliases]
* xref:cops_rails.adoc#railsapplicationcontroller[Rails/ApplicationController]
Expand Down
36 changes: 36 additions & 0 deletions docs/modules/ROOT/pages/cops_rails.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,42 @@ Book.update_attributes!(author: 'Alice')
Book.update!(author: 'Alice')
----

== Rails/ActiveRecordCallbacksOrder

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

| Pending
| Yes
| Yes
| 2.7
| -
|===

This cop checks that Active Record callbacks are declared
in the order in which they will be executed.

=== Examples

[source,ruby]
----
# bad
class Person < ApplicationRecord
after_commit :after_commit_callback
before_validation :before_validation_callback
end
# good
class Person < ApplicationRecord
before_validation :before_validation_callback
after_commit :after_commit_callback
end
----

=== References

* https://rails.rubystyle.guide/#callbacks-order

== Rails/ActiveRecordOverride

|===
Expand Down
145 changes: 145 additions & 0 deletions lib/rubocop/cop/rails/active_record_callbacks_order.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Rails
# This cop checks that Active Record callbacks are declared
# in the order in which they will be executed.
#
# @example
# # bad
# class Person < ApplicationRecord
# after_commit :after_commit_callback
# before_validation :before_validation_callback
# end
#
# # good
# class Person < ApplicationRecord
# before_validation :before_validation_callback
# after_commit :after_commit_callback
# end
#
class ActiveRecordCallbacksOrder < Cop
MSG = '`%<current>s` is supposed to appear before `%<previous>s`.'

CALLBACKS_IN_ORDER = %i[
after_initialize
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
before_update
around_update
after_update
before_destroy
around_destroy
after_destroy
after_save
after_commit
after_rollback
after_find
after_touch
].freeze

CALLBACKS_ORDER_MAP = Hash[
CALLBACKS_IN_ORDER.map.with_index { |name, index| [name, index] }
].freeze

def on_class(class_node)
previous_index = -1
previous_callback = nil

defined_callbacks(class_node).each do |node|
callback = node.method_name
index = CALLBACKS_ORDER_MAP[callback]

if index < previous_index
message = format(MSG, current: callback,
previous: previous_callback)
add_offense(node, message: message)
end
previous_index = index
previous_callback = callback
end
end

# Autocorrect by swapping between two nodes autocorrecting them
def autocorrect(node)
previous = left_siblings_of(node).find do |sibling|
callback?(sibling)
end

current_range = source_range_with_comment(node)
previous_range = source_range_with_comment(previous)

lambda do |corrector|
corrector.insert_before(previous_range, current_range.source)
corrector.remove(current_range)
end
end

private

def defined_callbacks(class_node)
class_def = class_node.body

if class_def
class_def.children.select { |c| callback?(c) }
else
[]
end
end

def callback?(node)
node.send_type? && CALLBACKS_ORDER_MAP.key?(node.method_name)
end

def left_siblings_of(node)
siblings_of(node)[0, node.sibling_index]
end

def siblings_of(node)
node.parent.children
end

def source_range_with_comment(node)
begin_pos = begin_pos_with_comment(node)
end_pos = end_position_for(node)

Parser::Source::Range.new(buffer, begin_pos, end_pos)
end

def end_position_for(node)
end_line = buffer.line_for_position(node.loc.expression.end_pos)
buffer.line_range(end_line).end_pos
end

def begin_pos_with_comment(node)
annotation_line = node.first_line - 1
first_comment = nil

processed_source.comments_before_line(annotation_line)
.reverse_each do |comment|
if comment.location.line == annotation_line
first_comment = comment
annotation_line -= 1
end
end

start_line_position(first_comment || node)
end

def start_line_position(node)
buffer.line_range(node.loc.line).begin_pos - 1
end

def buffer
processed_source.buffer
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rails_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

require_relative 'rails/action_filter'
require_relative 'rails/active_record_aliases'
require_relative 'rails/active_record_callbacks_order'
require_relative 'rails/active_record_override'
require_relative 'rails/active_support_aliases'
require_relative 'rails/application_controller'
Expand Down
73 changes: 73 additions & 0 deletions spec/rubocop/cop/rails/active_record_callbacks_order_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Rails::ActiveRecordCallbacksOrder do
subject(:cop) { described_class.new }

it 'registers an offense and corrects when declared callbacks are not correctly ordered' do
expect_offense(<<~RUBY)
class User < ApplicationRecord
scope :admins, -> { where(admin: true) }
after_commit :after_commit_callback
after_save :after_save_callback
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `after_save` is supposed to appear before `after_commit`.
def some_method
end
before_validation :before_validation_callback
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `before_validation` is supposed to appear before `after_save`.
some_other_macros :foo
end
RUBY

expect_correction(<<~RUBY)
class User < ApplicationRecord
scope :admins, -> { where(admin: true) }
before_validation :before_validation_callback
after_save :after_save_callback
after_commit :after_commit_callback
def some_method
end
some_other_macros :foo
end
RUBY
end

it 'correcly autocorrects where there is a comment for callback method' do
new_source = autocorrect_source(<<~RUBY)
class User < ApplicationRecord
# This is a comment for after_commit.
after_commit :after_commit_callback
after_save :after_save_callback
end
RUBY

expect(new_source).to eq(<<~RUBY)
class User < ApplicationRecord
after_save :after_save_callback
# This is a comment for after_commit.
after_commit :after_commit_callback
end
RUBY
end

it 'does not register an offense when declared callbacks are correctly ordered' do
expect_no_offenses(<<~RUBY)
class User < ApplicationRecord
scope :admins, -> { where(admin: true) }
before_validation :before_validation_callback
after_save :after_save_callback
def some_method
end
after_commit :after_commit_callback
end
RUBY
end
end

0 comments on commit 1328643

Please sign in to comment.