/
active_record_callbacks_order.rb
147 lines (123 loc) · 4.02 KB
/
active_record_callbacks_order.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# 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 = CALLBACKS_IN_ORDER.each_with_index.to_h.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).reverse_each.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.each_child_node.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 && !inline_comment?(comment)
first_comment = comment
annotation_line -= 1
end
end
start_line_position(first_comment || node)
end
def inline_comment?(comment)
!comment_line?(comment.loc.expression.source_line)
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