rubocop-asjer 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +14 -0
- data/config/default.yml +46 -8
- data/lib/rubocop/asjer/version.rb +1 -1
- data/lib/rubocop/cop/asjer/rails_class_order.rb +154 -88
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 447e874612f95f1d94bce6efa0da2fcfe9e06c1b72f624fb707ddc147c5280c3
|
|
4
|
+
data.tar.gz: a815ca7e0101b2e2d1bac4bfed12718ef10e95da304dfafa83c2bc74ef10b094
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59c42f2eb2390c5356d9afbe628a48652707c9ff3527306e2e83c29e48e9fbd04f1cfefd01f0d6b7d6da06198777d4a31721f2cce51319652ba24325d93a71a7
|
|
7
|
+
data.tar.gz: f0486413521d14aded52af6e496addd3f015c44ca832e393bb445b127c065f82e0aa0d3e906dd72bcda71439f5a451fbf68729afb89dec09182e7de06639d81a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.2](https://github.com/asjer/rubocop-asjer/compare/v0.4.1...v0.4.2) (2026-01-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* add missing rails classes to `RailsClassOrder` cop configuration ([0ddc16a](https://github.com/asjer/rubocop-asjer/commit/0ddc16a165530b0aa5c4e0d37195324779fca9a8))
|
|
9
|
+
|
|
10
|
+
## [0.4.1](https://github.com/asjer/rubocop-asjer/compare/v0.4.0...v0.4.1) (2026-01-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* improve autocorrect functionality for `RailsClassOrder` cop and add Ruby 4.0 to ci ([#23](https://github.com/asjer/rubocop-asjer/issues/23)) ([16fc637](https://github.com/asjer/rubocop-asjer/commit/16fc63770f05f74e2bc005becd136af7d8ccb0d1))
|
|
16
|
+
|
|
3
17
|
## [0.4.0](https://github.com/asjer/rubocop-asjer/compare/v0.3.1...v0.4.0) (2026-01-23)
|
|
4
18
|
|
|
5
19
|
|
data/config/default.yml
CHANGED
|
@@ -9,33 +9,71 @@ Asjer/RailsClassOrder:
|
|
|
9
9
|
VersionAdded: "0.4.0"
|
|
10
10
|
Include:
|
|
11
11
|
- 'app/models/**/*.rb'
|
|
12
|
+
Scopes:
|
|
13
|
+
- default_scope
|
|
14
|
+
- scope
|
|
15
|
+
Attributes:
|
|
16
|
+
- attr_accessor
|
|
17
|
+
- attr_reader
|
|
18
|
+
- attr_writer
|
|
19
|
+
- attr_readonly
|
|
20
|
+
- attribute
|
|
21
|
+
- serialize
|
|
22
|
+
- store
|
|
23
|
+
- store_accessor
|
|
24
|
+
Enums:
|
|
25
|
+
- enum
|
|
12
26
|
Associations:
|
|
13
27
|
- belongs_to
|
|
14
|
-
- has_many
|
|
15
28
|
- has_one
|
|
29
|
+
- has_many
|
|
16
30
|
- has_and_belongs_to_many
|
|
31
|
+
- has_one_attached
|
|
32
|
+
- has_many_attached
|
|
33
|
+
Validations:
|
|
34
|
+
- validates
|
|
35
|
+
- validates_acceptance_of
|
|
36
|
+
- validates_associated
|
|
37
|
+
- validates_comparison_of
|
|
38
|
+
- validates_confirmation_of
|
|
39
|
+
- validates_each
|
|
40
|
+
- validates_exclusion_of
|
|
41
|
+
- validates_format_of
|
|
42
|
+
- validates_inclusion_of
|
|
43
|
+
- validates_length_of
|
|
44
|
+
- validates_size_of
|
|
45
|
+
- validates_numericality_of
|
|
46
|
+
- validates_presence_of
|
|
47
|
+
- validates_uniqueness_of
|
|
48
|
+
- validates_with
|
|
49
|
+
- validate
|
|
17
50
|
Callbacks:
|
|
18
51
|
- after_initialize
|
|
19
52
|
- after_find
|
|
20
53
|
- after_touch
|
|
21
54
|
- before_validation
|
|
22
|
-
- validates
|
|
23
|
-
- validate
|
|
24
55
|
- after_validation
|
|
25
56
|
- before_save
|
|
26
57
|
- around_save
|
|
27
58
|
- before_create
|
|
28
59
|
- around_create
|
|
60
|
+
- after_create
|
|
29
61
|
- before_update
|
|
30
62
|
- around_update
|
|
63
|
+
- after_update
|
|
64
|
+
- after_save
|
|
31
65
|
- before_destroy
|
|
32
66
|
- around_destroy
|
|
33
67
|
- after_destroy
|
|
34
|
-
- after_update
|
|
35
|
-
- after_create
|
|
36
|
-
- after_save
|
|
37
68
|
- after_commit
|
|
38
69
|
- after_rollback
|
|
39
70
|
Others:
|
|
40
|
-
-
|
|
41
|
-
-
|
|
71
|
+
- encrypts
|
|
72
|
+
- normalizes
|
|
73
|
+
- delegate
|
|
74
|
+
- delegate_missing_to
|
|
75
|
+
- accepts_nested_attributes_for
|
|
76
|
+
- has_secure_password
|
|
77
|
+
- has_secure_token
|
|
78
|
+
- generates_token_for
|
|
79
|
+
- composed_of
|
|
@@ -5,11 +5,12 @@ module RuboCop
|
|
|
5
5
|
module Asjer
|
|
6
6
|
# Enforces consistent ordering of declarative methods in Rails models.
|
|
7
7
|
#
|
|
8
|
-
# Methods are grouped into
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# Methods are grouped into seven categories following Rails Style Guide:
|
|
9
|
+
# scopes, attributes, enums, associations, validations, callbacks, and others.
|
|
10
|
+
# Within each category, methods are sorted by their position in the configured list.
|
|
11
|
+
# Groups are separated by blank lines.
|
|
11
12
|
#
|
|
12
|
-
# The order is: associations,
|
|
13
|
+
# The order is: scopes, attributes, enums, associations, validations, callbacks, then others.
|
|
13
14
|
#
|
|
14
15
|
# @example
|
|
15
16
|
# # bad
|
|
@@ -18,7 +19,9 @@ module RuboCop
|
|
|
18
19
|
# validate :validate_name
|
|
19
20
|
# after_create :after_create_1
|
|
20
21
|
# has_many :messages
|
|
22
|
+
# scope :active, -> { where(active: true) }
|
|
21
23
|
# attr_readonly :email
|
|
24
|
+
# enum :status, [:pending, :active]
|
|
22
25
|
# after_create :after_create_2
|
|
23
26
|
# belongs_to :role
|
|
24
27
|
# before_create :set_name
|
|
@@ -26,34 +29,126 @@ module RuboCop
|
|
|
26
29
|
#
|
|
27
30
|
# # good
|
|
28
31
|
# class User < ApplicationRecord
|
|
32
|
+
# scope :active, -> { where(active: true) }
|
|
33
|
+
#
|
|
34
|
+
# attr_readonly :email
|
|
35
|
+
#
|
|
36
|
+
# enum :status, [:pending, :active]
|
|
37
|
+
#
|
|
29
38
|
# belongs_to :plan
|
|
30
39
|
# belongs_to :role
|
|
31
40
|
# has_many :messages
|
|
32
41
|
#
|
|
33
42
|
# validate :validate_name
|
|
43
|
+
#
|
|
34
44
|
# before_create :set_name
|
|
35
45
|
# after_create :after_create_1
|
|
36
46
|
# after_create :after_create_2
|
|
37
|
-
#
|
|
38
|
-
# attr_readonly :email
|
|
39
47
|
# end
|
|
40
48
|
#
|
|
41
49
|
# Default method lists for RailsClassOrder cop
|
|
42
50
|
module RailsClassOrderDefaults
|
|
51
|
+
SCOPES = %w[default_scope scope].freeze
|
|
52
|
+
|
|
53
|
+
ATTRIBUTES = %w[
|
|
54
|
+
attr_accessor attr_reader attr_writer attr_readonly
|
|
55
|
+
attribute serialize store store_accessor
|
|
56
|
+
].freeze
|
|
57
|
+
|
|
58
|
+
ENUMS = %w[enum].freeze
|
|
59
|
+
|
|
43
60
|
ASSOCIATIONS = %w[
|
|
44
|
-
belongs_to has_many
|
|
61
|
+
belongs_to has_one has_many has_and_belongs_to_many
|
|
62
|
+
has_one_attached has_many_attached
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
65
|
+
VALIDATIONS = %w[
|
|
66
|
+
validates validates_acceptance_of validates_associated
|
|
67
|
+
validates_comparison_of validates_confirmation_of validates_each
|
|
68
|
+
validates_exclusion_of validates_format_of validates_inclusion_of
|
|
69
|
+
validates_length_of validates_size_of validates_numericality_of
|
|
70
|
+
validates_presence_of validates_uniqueness_of validates_with
|
|
71
|
+
validate
|
|
45
72
|
].freeze
|
|
46
73
|
|
|
47
74
|
CALLBACKS = %w[
|
|
48
75
|
after_initialize after_find after_touch
|
|
49
|
-
before_validation
|
|
50
|
-
before_save around_save
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
before_validation after_validation
|
|
77
|
+
before_save around_save
|
|
78
|
+
before_create around_create after_create
|
|
79
|
+
before_update around_update after_update
|
|
80
|
+
after_save
|
|
81
|
+
before_destroy around_destroy after_destroy
|
|
53
82
|
after_commit after_rollback
|
|
54
83
|
].freeze
|
|
55
84
|
|
|
56
|
-
OTHERS = %w[
|
|
85
|
+
OTHERS = %w[
|
|
86
|
+
encrypts normalizes delegate delegate_missing_to
|
|
87
|
+
accepts_nested_attributes_for has_secure_password
|
|
88
|
+
has_secure_token generates_token_for composed_of
|
|
89
|
+
].freeze
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Autocorrect helpers for RailsClassOrder cop
|
|
93
|
+
module RailsClassOrderCorrector
|
|
94
|
+
def autocorrect(corrector, body, original, sorted)
|
|
95
|
+
first_target = original.min_by { |m| body.children.index(m) }
|
|
96
|
+
new_source = build_sorted_source(sorted, original)
|
|
97
|
+
corrector.replace(range_with_comments(first_target), new_source.rstrip)
|
|
98
|
+
|
|
99
|
+
(original - [first_target]).each do |method|
|
|
100
|
+
corrector.remove(full_method_range(method))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def range_with_comments(node)
|
|
105
|
+
comments = preceding_comments(node)
|
|
106
|
+
start_pos = comments.empty? ? node.loc.expression.begin_pos : comments.first.loc.expression.begin_pos
|
|
107
|
+
range_between(start_pos, node.loc.expression.end_pos)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def preceding_comments(node)
|
|
111
|
+
collect_adjacent_comments(node.loc.expression)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def collect_adjacent_comments(node_pos)
|
|
115
|
+
expected_line = node_pos.first_line - 1
|
|
116
|
+
comments_before_node(node_pos).take_while do |comment|
|
|
117
|
+
pos = comment.loc.expression
|
|
118
|
+
(pos.last_line == expected_line).tap { expected_line = pos.first_line - 1 }
|
|
119
|
+
end.reverse
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def comments_before_node(node_pos)
|
|
123
|
+
processed_source.comments.select { |c| c.loc.expression.end_pos < node_pos.begin_pos }.reverse
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def full_method_range(node)
|
|
127
|
+
range = range_with_comments(node)
|
|
128
|
+
source = processed_source.buffer.source
|
|
129
|
+
line_start = source.rindex("\n", range.begin_pos - 1)&.+(1) || 0
|
|
130
|
+
end_pos = source[range.end_pos] == "\n" ? range.end_pos + 1 : range.end_pos
|
|
131
|
+
range_between(line_start, end_pos)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_sorted_source(sorted, original)
|
|
135
|
+
indent = ' ' * original.first.loc.column
|
|
136
|
+
grouped = sorted.group_by { |m| method_type(m) }
|
|
137
|
+
format_grouped_source(grouped, indent)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def format_grouped_source(grouped, indent)
|
|
141
|
+
self.class::TYPE_ORDER.keys.filter_map do |type|
|
|
142
|
+
next unless grouped[type]&.any?
|
|
143
|
+
|
|
144
|
+
grouped[type].map { |m| source_with_comments(m) }.join("\n#{indent}")
|
|
145
|
+
end.join("\n\n#{indent}")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def source_with_comments(node)
|
|
149
|
+
range = range_with_comments(node)
|
|
150
|
+
processed_source.buffer.source[range.begin_pos...range.end_pos].lstrip
|
|
151
|
+
end
|
|
57
152
|
end
|
|
58
153
|
|
|
59
154
|
# Enforces consistent ordering of declarative methods in Rails models.
|
|
@@ -62,66 +157,79 @@ module RuboCop
|
|
|
62
157
|
class RailsClassOrder < Base
|
|
63
158
|
extend AutoCorrector
|
|
64
159
|
include RangeHelp
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
160
|
+
include RailsClassOrderCorrector
|
|
161
|
+
|
|
162
|
+
MSG = 'Declarative methods should be sorted by type: scopes, attributes, enums, ' \
|
|
163
|
+
'associations, validations, callbacks, then others.'
|
|
164
|
+
|
|
165
|
+
TYPE_ORDER = {
|
|
166
|
+
scope: 0,
|
|
167
|
+
attribute: 1,
|
|
168
|
+
enum: 2,
|
|
169
|
+
association: 3,
|
|
170
|
+
validation: 4,
|
|
171
|
+
callback: 5,
|
|
172
|
+
other: 6
|
|
173
|
+
}.freeze
|
|
174
|
+
|
|
175
|
+
CATEGORY_CONFIG = {
|
|
176
|
+
scope: { key: 'Scopes', const: :SCOPES },
|
|
177
|
+
attribute: { key: 'Attributes', const: :ATTRIBUTES },
|
|
178
|
+
enum: { key: 'Enums', const: :ENUMS },
|
|
179
|
+
association: { key: 'Associations', const: :ASSOCIATIONS },
|
|
180
|
+
validation: { key: 'Validations', const: :VALIDATIONS },
|
|
181
|
+
callback: { key: 'Callbacks', const: :CALLBACKS },
|
|
182
|
+
other: { key: 'Others', const: :OTHERS }
|
|
183
|
+
}.freeze
|
|
69
184
|
|
|
70
185
|
def on_class(node)
|
|
71
186
|
_name, _superclass, body = *node
|
|
72
187
|
return unless body&.begin_type?
|
|
73
188
|
|
|
189
|
+
check_order(body)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
def check_order(body)
|
|
74
195
|
targets = target_methods(body)
|
|
75
196
|
return if targets.empty?
|
|
76
197
|
|
|
77
198
|
sorted = sort_methods(targets)
|
|
78
199
|
return if targets == sorted
|
|
79
200
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
end
|
|
201
|
+
first_misplaced = targets.zip(sorted).find { |a, e| a != e }&.first
|
|
202
|
+
add_offense(first_misplaced) { |corrector| autocorrect(corrector, body, targets, sorted) }
|
|
83
203
|
end
|
|
84
204
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@callbacks ||= cop_config.fetch('Callbacks', RailsClassOrderDefaults::CALLBACKS).map(&:to_sym)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def others
|
|
96
|
-
@others ||= cop_config.fetch('Others', RailsClassOrderDefaults::OTHERS).map(&:to_sym)
|
|
205
|
+
def category_methods(category)
|
|
206
|
+
cfg = CATEGORY_CONFIG[category]
|
|
207
|
+
instance_variable_get(:"@#{category}") ||
|
|
208
|
+
instance_variable_set(
|
|
209
|
+
:"@#{category}",
|
|
210
|
+
cop_config.fetch(cfg[:key], RailsClassOrderDefaults.const_get(cfg[:const])).map(&:to_sym)
|
|
211
|
+
)
|
|
97
212
|
end
|
|
98
213
|
|
|
99
214
|
def all_target_methods
|
|
100
|
-
@all_target_methods ||=
|
|
215
|
+
@all_target_methods ||= TYPE_ORDER.keys.flat_map { |cat| category_methods(cat) }
|
|
101
216
|
end
|
|
102
217
|
|
|
103
218
|
def target_methods(body)
|
|
104
|
-
body.children.select
|
|
105
|
-
child.send_type? && all_target_methods.include?(child.method_name)
|
|
106
|
-
end
|
|
219
|
+
body.children.select { |child| child.send_type? && all_target_methods.include?(child.method_name) }
|
|
107
220
|
end
|
|
108
221
|
|
|
109
222
|
def sort_methods(methods)
|
|
110
|
-
# Use sort_by with index to make stable sort (preserve original order for equal elements)
|
|
111
223
|
methods.each_with_index.sort_by do |method, index|
|
|
112
|
-
[
|
|
113
|
-
method_type_order(method),
|
|
114
|
-
method_position_in_type(method),
|
|
115
|
-
index
|
|
116
|
-
]
|
|
224
|
+
[method_type_order(method), method_position_in_type(method), index]
|
|
117
225
|
end.map(&:first)
|
|
118
226
|
end
|
|
119
227
|
|
|
120
228
|
def method_type(method)
|
|
121
229
|
name = method.method_name
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
230
|
+
TYPE_ORDER.each_key do |category|
|
|
231
|
+
return category if category_methods(category).include?(name)
|
|
232
|
+
end
|
|
125
233
|
:other
|
|
126
234
|
end
|
|
127
235
|
|
|
@@ -130,50 +238,8 @@ module RuboCop
|
|
|
130
238
|
end
|
|
131
239
|
|
|
132
240
|
def method_position_in_type(method)
|
|
133
|
-
|
|
134
|
-
list
|
|
135
|
-
list.index(name) || list.size
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def method_list_for_type(type)
|
|
139
|
-
{ association: associations, callback: callbacks, other: others }[type]
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def contiguous?(body, targets)
|
|
143
|
-
indices = targets.map { |t| body.children.index(t) }
|
|
144
|
-
indices.max - indices.min + 1 == indices.size
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def autocorrect(corrector, _body, original, sorted)
|
|
148
|
-
grouped = group_by_type(sorted)
|
|
149
|
-
new_source = build_grouped_source(grouped, original)
|
|
150
|
-
range = methods_range(original)
|
|
151
|
-
corrector.replace(range, new_source)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def group_by_type(methods)
|
|
155
|
-
methods.group_by { |m| method_type(m) }
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def build_grouped_source(grouped, original)
|
|
159
|
-
indent = ' ' * original.first.loc.column
|
|
160
|
-
|
|
161
|
-
groups = []
|
|
162
|
-
%i[association callback other].each do |type|
|
|
163
|
-
next unless grouped[type]&.any?
|
|
164
|
-
|
|
165
|
-
group_lines = grouped[type].map(&:source)
|
|
166
|
-
groups << group_lines.join("\n#{indent}")
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
groups.join("\n\n#{indent}")
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def methods_range(methods)
|
|
173
|
-
first = methods.min_by { |m| m.loc.expression.begin_pos }
|
|
174
|
-
last = methods.max_by { |m| m.loc.expression.end_pos }
|
|
175
|
-
|
|
176
|
-
range_between(first.loc.expression.begin_pos, last.loc.expression.end_pos)
|
|
241
|
+
list = category_methods(method_type(method))
|
|
242
|
+
list.index(method.method_name) || list.size
|
|
177
243
|
end
|
|
178
244
|
end
|
|
179
245
|
end
|