rubocop-asjer 0.3.0 → 0.4.0
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/no_default_translation.rb +14 -4
- data/lib/rubocop/cop/asjer/rails_class_order.rb +181 -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: 772cbb0491de18ffeb19b27e41336474005b2c9295f5f2c152c842883e250654
|
|
4
|
+
data.tar.gz: f2b5e866ba2bccd3fee26d59868f80b8f12de62e57b161e203e932f8aa70533f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 891112aad96477909e4a2de0a24660655b5573b6ef8f837cd70fe84927502390a3544f47994a9644b4d5b12b826aba74c8ab09f73dde3d49b044bb5fa6cc62cf
|
|
7
|
+
data.tar.gz: f6c1dd35c8a2fe82392a38576b5c26af9b4958ad732d35493d4ae8e35cd0f423610b4263ff80909ce31c99f28d044a979eb8c0d124a5d4738777f984dc753fb6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/asjer/rubocop-asjer/compare/v0.3.1...v0.4.0) (2026-01-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* 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))
|
|
9
|
+
|
|
10
|
+
## [0.3.1](https://github.com/asjer/rubocop-asjer/compare/v0.3.0...v0.3.1) (2026-01-03)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* enable autocorrect for all `default:` option cases in `NoDefaultTranslation` ([fbd359e](https://github.com/asjer/rubocop-asjer/commit/fbd359e6f252188880535c87b3f5abbbd8bac571))
|
|
16
|
+
|
|
3
17
|
## [0.3.0](https://github.com/asjer/rubocop-asjer/compare/v0.2.0...v0.3.0) (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
|
@@ -34,9 +34,6 @@ module RuboCop
|
|
|
34
34
|
def on_send(node)
|
|
35
35
|
translation_with_default?(node) do |default_pair|
|
|
36
36
|
add_offense(default_pair) do |corrector|
|
|
37
|
-
# Skip autocorrection if default: is the only hash option
|
|
38
|
-
next if default_pair.parent.pairs.size == 1
|
|
39
|
-
|
|
40
37
|
corrector.remove(removal_range(default_pair))
|
|
41
38
|
end
|
|
42
39
|
end
|
|
@@ -45,7 +42,20 @@ module RuboCop
|
|
|
45
42
|
private
|
|
46
43
|
|
|
47
44
|
def removal_range(node)
|
|
48
|
-
|
|
45
|
+
hash_node = node.parent
|
|
46
|
+
pairs = hash_node.pairs
|
|
47
|
+
|
|
48
|
+
return hash_with_comma_range(hash_node) if pairs.size == 1
|
|
49
|
+
|
|
50
|
+
pair_removal_range(node, pairs)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def hash_with_comma_range(hash_node)
|
|
54
|
+
with_space = range_with_surrounding_space(range: hash_node.source_range, side: :left)
|
|
55
|
+
range_with_surrounding_comma(with_space, :left)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def pair_removal_range(node, pairs)
|
|
49
59
|
index = pairs.index(node)
|
|
50
60
|
|
|
51
61
|
last_pair?(index, pairs) ? leading_range(node, pairs[index - 1]) : trailing_range(node, pairs[index + 1])
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
# Enforces consistent ordering of declarative methods in Rails models.
|
|
60
|
+
#
|
|
61
|
+
# @see RailsClassOrderDefaults for default method lists
|
|
62
|
+
class RailsClassOrder < Base
|
|
63
|
+
extend AutoCorrector
|
|
64
|
+
include RangeHelp
|
|
65
|
+
|
|
66
|
+
MSG = 'Declarative methods should be sorted by type: associations, callbacks, then others.'
|
|
67
|
+
|
|
68
|
+
TYPE_ORDER = { association: 0, callback: 1, other: 2 }.freeze
|
|
69
|
+
|
|
70
|
+
def on_class(node)
|
|
71
|
+
_name, _superclass, body = *node
|
|
72
|
+
return unless body&.begin_type?
|
|
73
|
+
|
|
74
|
+
targets = target_methods(body)
|
|
75
|
+
return if targets.empty?
|
|
76
|
+
|
|
77
|
+
sorted = sort_methods(targets)
|
|
78
|
+
return if targets == sorted
|
|
79
|
+
|
|
80
|
+
add_offense(body) do |corrector|
|
|
81
|
+
autocorrect(corrector, body, targets, sorted) if contiguous?(body, targets)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def associations
|
|
88
|
+
@associations ||= cop_config.fetch('Associations', RailsClassOrderDefaults::ASSOCIATIONS).map(&:to_sym)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def callbacks
|
|
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)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def all_target_methods
|
|
100
|
+
@all_target_methods ||= associations + callbacks + others
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def target_methods(body)
|
|
104
|
+
body.children.select do |child|
|
|
105
|
+
child.send_type? && all_target_methods.include?(child.method_name)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def sort_methods(methods)
|
|
110
|
+
# Use sort_by with index to make stable sort (preserve original order for equal elements)
|
|
111
|
+
methods.each_with_index.sort_by do |method, index|
|
|
112
|
+
[
|
|
113
|
+
method_type_order(method),
|
|
114
|
+
method_position_in_type(method),
|
|
115
|
+
index
|
|
116
|
+
]
|
|
117
|
+
end.map(&:first)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def method_type(method)
|
|
121
|
+
name = method.method_name
|
|
122
|
+
return :association if associations.include?(name)
|
|
123
|
+
return :callback if callbacks.include?(name)
|
|
124
|
+
|
|
125
|
+
:other
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def method_type_order(method)
|
|
129
|
+
TYPE_ORDER[method_type(method)]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def method_position_in_type(method)
|
|
133
|
+
name = method.method_name
|
|
134
|
+
list = method_list_for_type(method_type(method))
|
|
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)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
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.0
|
|
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
|