rubocop-asjer 0.3.1 → 0.4.1
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/README.md +52 -0
- data/config/default.yml +37 -0
- data/lib/rubocop/asjer/version.rb +1 -1
- data/lib/rubocop/asjer.rb +1 -0
- data/lib/rubocop/cop/asjer/rails_class_order.rb +195 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6401e9b5d10c63a9c3ff7f3292aac36e8368a54fdd4df3062efda177ee331c11
|
|
4
|
+
data.tar.gz: a7e90b0a705e59b161b575fdedb087d82b54dd8af3cfec7509bc54553012c5f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0f223e4e32b83736f21750b9ebb06ab49eacd9e2ccb45d37f047b231d763cb4f4dd1effa7bb87e0376f04db89287b48a7727c544b84b96585b657a02d2fa092
|
|
7
|
+
data.tar.gz: '0810af3f8f3b56e7ae3b4ee4295805a9a01ee0cb34911da8d6197db60ff1d0f8ad3a37d596d8411ced98e2eae0fb13c60f904903bebea26b2302b52cdc66d3de'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.1](https://github.com/asjer/rubocop-asjer/compare/v0.4.0...v0.4.1) (2026-01-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* 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))
|
|
9
|
+
|
|
10
|
+
## [0.4.0](https://github.com/asjer/rubocop-asjer/compare/v0.3.1...v0.4.0) (2026-01-23)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add `RailsClassOrder` cop to enforce method ordering in Rails models ([#20](https://github.com/asjer/rubocop-asjer/issues/20)) ([bce5d9b](https://github.com/asjer/rubocop-asjer/commit/bce5d9b2affbf207ad34c99d574340d7437e3a04))
|
|
16
|
+
|
|
3
17
|
## [0.3.1](https://github.com/asjer/rubocop-asjer/compare/v0.3.0...v0.3.1) (2026-01-03)
|
|
4
18
|
|
|
5
19
|
|
data/README.md
CHANGED
|
@@ -44,6 +44,58 @@ t('some.key')
|
|
|
44
44
|
I18n.t('some.key')
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### Asjer/RailsClassOrder
|
|
48
|
+
|
|
49
|
+
Enforces consistent ordering of declarative methods in Rails models. Methods are grouped into three categories: associations, callbacks, and others. Groups are separated by blank lines.
|
|
50
|
+
|
|
51
|
+
**Supports autocorrect** with `rubocop -a` or `rubocop -A`.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# bad
|
|
55
|
+
class User < ApplicationRecord
|
|
56
|
+
belongs_to :plan
|
|
57
|
+
validate :validate_name
|
|
58
|
+
after_create :after_create_1
|
|
59
|
+
has_many :messages
|
|
60
|
+
attr_readonly :email
|
|
61
|
+
after_create :after_create_2
|
|
62
|
+
belongs_to :role
|
|
63
|
+
before_create :set_name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# good
|
|
67
|
+
class User < ApplicationRecord
|
|
68
|
+
belongs_to :plan
|
|
69
|
+
belongs_to :role
|
|
70
|
+
has_many :messages
|
|
71
|
+
|
|
72
|
+
validate :validate_name
|
|
73
|
+
before_create :set_name
|
|
74
|
+
after_create :after_create_1
|
|
75
|
+
after_create :after_create_2
|
|
76
|
+
|
|
77
|
+
attr_readonly :email
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
By default, this cop only runs on files matching `app/models/**/*.rb`. The method lists are fully configurable:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
Asjer/RailsClassOrder:
|
|
85
|
+
Associations:
|
|
86
|
+
- belongs_to
|
|
87
|
+
- has_many
|
|
88
|
+
- has_one
|
|
89
|
+
- has_and_belongs_to_many
|
|
90
|
+
Callbacks:
|
|
91
|
+
- after_initialize
|
|
92
|
+
- after_find
|
|
93
|
+
# ... (see config/default.yml for full list)
|
|
94
|
+
Others:
|
|
95
|
+
- attr_readonly
|
|
96
|
+
- serialize
|
|
97
|
+
```
|
|
98
|
+
|
|
47
99
|
## Development
|
|
48
100
|
|
|
49
101
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
data/config/default.yml
CHANGED
|
@@ -2,3 +2,40 @@ Asjer/NoDefaultTranslation:
|
|
|
2
2
|
Description: Checks for use of the `default:` option in translation calls.
|
|
3
3
|
Enabled: true
|
|
4
4
|
VersionAdded: "0.1.0"
|
|
5
|
+
|
|
6
|
+
Asjer/RailsClassOrder:
|
|
7
|
+
Description: Enforces consistent ordering of declarative methods in Rails models.
|
|
8
|
+
Enabled: true
|
|
9
|
+
VersionAdded: "0.4.0"
|
|
10
|
+
Include:
|
|
11
|
+
- 'app/models/**/*.rb'
|
|
12
|
+
Associations:
|
|
13
|
+
- belongs_to
|
|
14
|
+
- has_many
|
|
15
|
+
- has_one
|
|
16
|
+
- has_and_belongs_to_many
|
|
17
|
+
Callbacks:
|
|
18
|
+
- after_initialize
|
|
19
|
+
- after_find
|
|
20
|
+
- after_touch
|
|
21
|
+
- before_validation
|
|
22
|
+
- validates
|
|
23
|
+
- validate
|
|
24
|
+
- after_validation
|
|
25
|
+
- before_save
|
|
26
|
+
- around_save
|
|
27
|
+
- before_create
|
|
28
|
+
- around_create
|
|
29
|
+
- before_update
|
|
30
|
+
- around_update
|
|
31
|
+
- before_destroy
|
|
32
|
+
- around_destroy
|
|
33
|
+
- after_destroy
|
|
34
|
+
- after_update
|
|
35
|
+
- after_create
|
|
36
|
+
- after_save
|
|
37
|
+
- after_commit
|
|
38
|
+
- after_rollback
|
|
39
|
+
Others:
|
|
40
|
+
- attr_readonly
|
|
41
|
+
- serialize
|
data/lib/rubocop/asjer.rb
CHANGED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Asjer
|
|
6
|
+
# Enforces consistent ordering of declarative methods in Rails models.
|
|
7
|
+
#
|
|
8
|
+
# Methods are grouped into three categories: associations, callbacks,
|
|
9
|
+
# and others. Within each category, methods are sorted by their position
|
|
10
|
+
# in the configured list. Groups are separated by blank lines.
|
|
11
|
+
#
|
|
12
|
+
# The order is: associations, then callbacks, then others.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# class User < ApplicationRecord
|
|
17
|
+
# belongs_to :plan
|
|
18
|
+
# validate :validate_name
|
|
19
|
+
# after_create :after_create_1
|
|
20
|
+
# has_many :messages
|
|
21
|
+
# attr_readonly :email
|
|
22
|
+
# after_create :after_create_2
|
|
23
|
+
# belongs_to :role
|
|
24
|
+
# before_create :set_name
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # good
|
|
28
|
+
# class User < ApplicationRecord
|
|
29
|
+
# belongs_to :plan
|
|
30
|
+
# belongs_to :role
|
|
31
|
+
# has_many :messages
|
|
32
|
+
#
|
|
33
|
+
# validate :validate_name
|
|
34
|
+
# before_create :set_name
|
|
35
|
+
# after_create :after_create_1
|
|
36
|
+
# after_create :after_create_2
|
|
37
|
+
#
|
|
38
|
+
# attr_readonly :email
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# Default method lists for RailsClassOrder cop
|
|
42
|
+
module RailsClassOrderDefaults
|
|
43
|
+
ASSOCIATIONS = %w[
|
|
44
|
+
belongs_to has_many has_one has_and_belongs_to_many
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
CALLBACKS = %w[
|
|
48
|
+
after_initialize after_find after_touch
|
|
49
|
+
before_validation validates validate after_validation
|
|
50
|
+
before_save around_save before_create around_create
|
|
51
|
+
before_update around_update before_destroy around_destroy
|
|
52
|
+
after_destroy after_update after_create after_save
|
|
53
|
+
after_commit after_rollback
|
|
54
|
+
].freeze
|
|
55
|
+
|
|
56
|
+
OTHERS = %w[attr_readonly serialize].freeze
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Autocorrect helpers for RailsClassOrder cop
|
|
60
|
+
module RailsClassOrderCorrector
|
|
61
|
+
def autocorrect(corrector, body, original, sorted)
|
|
62
|
+
first_target = original.min_by { |m| body.children.index(m) }
|
|
63
|
+
new_source = build_sorted_source(sorted, original)
|
|
64
|
+
corrector.replace(range_with_comments(first_target), new_source.rstrip)
|
|
65
|
+
|
|
66
|
+
(original - [first_target]).each do |method|
|
|
67
|
+
corrector.remove(full_method_range(method))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def range_with_comments(node)
|
|
72
|
+
comments = preceding_comments(node)
|
|
73
|
+
start_pos = comments.empty? ? node.loc.expression.begin_pos : comments.first.loc.expression.begin_pos
|
|
74
|
+
range_between(start_pos, node.loc.expression.end_pos)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def preceding_comments(node)
|
|
78
|
+
collect_adjacent_comments(node.loc.expression)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def collect_adjacent_comments(node_pos)
|
|
82
|
+
expected_line = node_pos.first_line - 1
|
|
83
|
+
comments_before_node(node_pos).take_while do |comment|
|
|
84
|
+
pos = comment.loc.expression
|
|
85
|
+
(pos.last_line == expected_line).tap { expected_line = pos.first_line - 1 }
|
|
86
|
+
end.reverse
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def comments_before_node(node_pos)
|
|
90
|
+
processed_source.comments.select { |c| c.loc.expression.end_pos < node_pos.begin_pos }.reverse
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def full_method_range(node)
|
|
94
|
+
range = range_with_comments(node)
|
|
95
|
+
source = processed_source.buffer.source
|
|
96
|
+
line_start = source.rindex("\n", range.begin_pos - 1)&.+(1) || 0
|
|
97
|
+
end_pos = source[range.end_pos] == "\n" ? range.end_pos + 1 : range.end_pos
|
|
98
|
+
range_between(line_start, end_pos)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_sorted_source(sorted, original)
|
|
102
|
+
indent = ' ' * original.first.loc.column
|
|
103
|
+
grouped = sorted.group_by { |m| method_type(m) }
|
|
104
|
+
|
|
105
|
+
%i[association callback other].filter_map do |type|
|
|
106
|
+
next unless grouped[type]&.any?
|
|
107
|
+
|
|
108
|
+
grouped[type].map { |m| source_with_comments(m) }.join("\n#{indent}")
|
|
109
|
+
end.join("\n\n#{indent}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def source_with_comments(node)
|
|
113
|
+
range = range_with_comments(node)
|
|
114
|
+
processed_source.buffer.source[range.begin_pos...range.end_pos].lstrip
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Enforces consistent ordering of declarative methods in Rails models.
|
|
119
|
+
#
|
|
120
|
+
# @see RailsClassOrderDefaults for default method lists
|
|
121
|
+
class RailsClassOrder < Base
|
|
122
|
+
extend AutoCorrector
|
|
123
|
+
include RangeHelp
|
|
124
|
+
include RailsClassOrderCorrector
|
|
125
|
+
|
|
126
|
+
MSG = 'Declarative methods should be sorted by type: associations, callbacks, then others.'
|
|
127
|
+
TYPE_ORDER = { association: 0, callback: 1, other: 2 }.freeze
|
|
128
|
+
|
|
129
|
+
def on_class(node)
|
|
130
|
+
_name, _superclass, body = *node
|
|
131
|
+
return unless body&.begin_type?
|
|
132
|
+
|
|
133
|
+
check_order(body)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def check_order(body)
|
|
139
|
+
targets = target_methods(body)
|
|
140
|
+
return if targets.empty?
|
|
141
|
+
|
|
142
|
+
sorted = sort_methods(targets)
|
|
143
|
+
return if targets == sorted
|
|
144
|
+
|
|
145
|
+
first_misplaced = targets.zip(sorted).find { |a, e| a != e }&.first
|
|
146
|
+
add_offense(first_misplaced) { |corrector| autocorrect(corrector, body, targets, sorted) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def associations
|
|
150
|
+
@associations ||= cop_config.fetch('Associations', RailsClassOrderDefaults::ASSOCIATIONS).map(&:to_sym)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def callbacks
|
|
154
|
+
@callbacks ||= cop_config.fetch('Callbacks', RailsClassOrderDefaults::CALLBACKS).map(&:to_sym)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def others
|
|
158
|
+
@others ||= cop_config.fetch('Others', RailsClassOrderDefaults::OTHERS).map(&:to_sym)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def all_target_methods
|
|
162
|
+
@all_target_methods ||= associations + callbacks + others
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def target_methods(body)
|
|
166
|
+
body.children.select { |child| child.send_type? && all_target_methods.include?(child.method_name) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def sort_methods(methods)
|
|
170
|
+
methods.each_with_index.sort_by { |m, i| [method_type_order(m), method_position_in_type(m), i] }.map(&:first)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def method_type(method)
|
|
174
|
+
name = method.method_name
|
|
175
|
+
return :association if associations.include?(name)
|
|
176
|
+
return :callback if callbacks.include?(name)
|
|
177
|
+
|
|
178
|
+
:other
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def method_type_order(method)
|
|
182
|
+
TYPE_ORDER[method_type(method)]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def method_position_in_type(method)
|
|
186
|
+
method_list_for_type(method_type(method)).index(method.method_name) || 999
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def method_list_for_type(type)
|
|
190
|
+
{ association: associations, callback: callbacks, other: others }[type]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-asjer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asjer Querido
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/rubocop/asjer/plugin.rb
|
|
58
58
|
- lib/rubocop/asjer/version.rb
|
|
59
59
|
- lib/rubocop/cop/asjer/no_default_translation.rb
|
|
60
|
+
- lib/rubocop/cop/asjer/rails_class_order.rb
|
|
60
61
|
- release-please-config.json
|
|
61
62
|
- sig/rubocop/i18n.rbs
|
|
62
63
|
homepage: https://github.com/asjer/rubocop-asjer
|